テストクラスの基本
テストクラス
/**
* @description テストクラスの説明
*/
@IsTest
private class SampleTest {
@TestSetup
static void setup() {
// 共通で使用するテストレコードの作成
}
/**
* @description テストメソッドの説明
*/
@IsTest
static void myTest() {
// テストデータの準備
Test.startTest();
// テスト対象処理の実行
Test.endTest();
// テスト結果の検証
}
}
アサーションメソッド
Assert.areEqual(expected, actual, msg);
Assert.areNotEqual(notExpected, actual, msg);
Assert.isTrue(condition, msg);
Assert.isFalse(condition, msg);
Assert.isNull(value, msg);
Assert.isNotNull(value, msg);
Assert.isInstanceOfType(instance, expectedType, msg);
Assert.isNotInstanceOfType(instance, notExpectedType, msg);
Assert.fail(msg);
ケース別テスト
例外
try {
Calculator.divide(3, 0);
// Assert.failを使って、例外が発生しない場合テストを失敗させる。
Assert.fail('Exception failure is always an option');
} catch (Exception e) {
// 発生した例外が意図した例外かをisInstanceOfTypeで検証する。
Assert.isInstanceOfType(e, DmlException.class);
}
非同期処理
非同期処理をテストするには、非同期処理をTest.startTest()
とTest.stopTest()
で挟む必要がある。Test.stopTest()
によって、この間に行われた非同期処理が同期的に実行され、結果を確認することができる。
@IsTest
static void futureTest() {
Test.startTest();
SampleService.doSomethingAsync('テスト');
Test.stopTest();
Assert.areEqual(1, Limits.getFutureCalls(), 'Futureメソッドのコール回数');
}
メール送信
テストでメール送信処理を行ってもメールは送信されない。送信処理が実行されることを確認するにはLimits.getEmailInvocations()
を使用する。このメソッドはTest.startTest()
と同時に使用することはできないため、非同期でメール送信する場合は非同期処理とメール送信処理で分けてテストする必要がある。
@IsTest
static void sendEmailTest() {
EmailService.sendEmail('テスト'); // メール送信を行う同期処理
Assert.areEqual(1, Limits.getEmailInvocations(), 'メール送信のコール回数');
}
Apex REST コールアウト
モックを使用する。
レスポンスを返す値を動的にしたい場合は、Mockにインスタンス変数を持たせるとよい。
モックを使用しないと下記のエラーが発生する。
TestMethod として定義されたメソッドは、Web サービスコールアウトをサポートしません。
@IsTest
static void testCallout() {
Test.setMock(HttpCalloutMock.class, new SampleCalloutServiceMock());
Test.startTest();
HttpResponse res = SampleCalloutService.runSampleSync();
Test.stopTest();
Assert.areEqual(200, res.getStatusCode(), 'fake response code');
}
private class SampleCalloutService200Mock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
Assert.areEqual('https://example.com/example/test', req.getEndpoint());
Assert.areEqual('GET', req.getMethod());
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setStatus('Complete');
res.setHeader('Content-Type', 'application/json');
res.setBody('[{"_id": "55d66226726b611100aaf741"}]');
return res;
}
}
- HttpCalloutMock インターフェースの実装による HTTP コールアウトのテスト | Apex 開発者ガイド
- 静的リソースを使用した HTTP コールアウトのテスト | Apex 開発者ガイド
Apex REST サービス
RestContextを使用する。Restサービスクラスで戻り値の逐次化をしている場合は戻り値で検証可能。逐次化していない場合、例のようにRestContext.response.responseBody
を使用して検証する。
// リクエストの作成
RestRequest request = new RestRequest();
request.requestUri = '/applications';
request.httpMethod = 'POST';
request.addHeader('Content-Type', 'application/json');
request.requestBody = Blob.valueOf(createTestDataJson());
RestContext.request = request;
RestContext.response = new RestResponse();
Test.startTest();
APP_RestApplicationsResource.doPost();
Test.stopTest();
RestResponse response = RestContext.response;
System.debug(LoggingLevel.DEBUG, response.responseBody?.toString());
// レスポンスの検証
Assert.areEqual(201, response.statusCode, 'ステータスコード');
Map<String, Object> responseBody = (Map<String, Object>) JSON.deserializeUntyped(response.responseBody.toString());
Assert.isNotNull(responseBody.applicationNumber, '申請番号');
Visualforceコントローラ
@IsTest
static void doSomethingTest() {
// コントローラを利用するVisualforceページをテスト実行時の画面として設定
PageReference pageRef = Page.SamplePage;
pageRef.getParameters().put('key', 'value');
Test.setCurrentPage(pageRef);
// テスト対象のメソッドを実行
Test.startTest();
SamplePageController ctrl = new SamplePageController();
PageReference nextPageRef = ctrl.doSomething();
Test.stopTest();
// テスト結果の検証
Assert.areEqual('/apex/success', nextPageRef.getUrl());
}
変更データキャプチャ
Test.enableChangeDataCapture
で変更イベント通知の生成を有効化し、DML操作を行い、Test.getEventBus().deliver()
で変更イベントを配信する。
Test.startTest()
を併用する場合、Test.enableChangeDataCapture()
よりあとにTest.startTest()
が来るようにしないとエラーになる。
// 変更イベント通知の生成を有効にする
Test.enableChangeDataCapture();
insert TestDataFactory.createAccount();
Test.startTest();
// テスト変更イベントの配信
Test.getEventBus().deliver();
Test.stopTest();
Big Objects
Big Objectsをテストで扱うことはできない。そのため、スタブを使用する。
スタブはTrailhead Sample AppのApexRecipeのTestDoubleを使用すると簡単に作成できる。
// Big Objectsを操作するクラスを指定してTestDoubleインスタンスを作成する。
TestDouble stub = new TestDouble(BigObjectSampleDmlService.class);
// スタブにメソッドを登録する。
TestDouble.Method methodToTrack = new TestDouble.Method('insertBigObjectSample');
stub.track(methodToTrack);
// テスト対象のクラスに、スタブを渡す。
SampleConsumer.sampleDmlService = (BigObjectSampleDmlService) stub.generate();
Test.startTest();
SampleConsumer.run();
Test.stopTest();
// スタブのメソッド実行回数を検証で使用することができる。
Assert.areEqual(1, methodToTrack.hasBeenCalledXTimes, 'メソッド実行回数');
例えばトリガハンドラでBig Objectsを使用する場合は、下記のようになる。
public with sharing class AccountChangeEventTriggerHandler extends TriggerHandler {
@TestVisible
private static RecordChangeLogService changeLogService = new RecordChangeLogService();
// 略
}
@IsTest
static void insertRecordChangeLogTest() {
TestDouble stub = new TestDouble(RecordChangeLogService.class);
TestDouble.Method methodToTrack = new TestDouble.Method('insertRecordChangeLog');
stub.track(methodToTrack);
AccountChangeEventTriggerHandler.changeLogService = (RecordChangeLogService) stub.generate();
// 略
}
- Apex を使用してカスタム Big Object を設定する | Big Objects 実装ガイド
- スタブ API を使用してモッキング フレームワークを構築する | Apex 開発者ガイド
- TestDouble.cls at main · trailheadapps/apex-recipes
SOSLを含む処理のテスト
テストではSOSLの結果が空のリストになる。SOSLの結果に特定の値を返すようにするためには、Test.setFixedSearchResults
メソッドを使用する。
@IsTest
static void searchUserTest() {
Test.setFixedSearchResults(new List<Id>{ UserInfo.getUserId() });
Test.startTest();
List<User> results = SampleClass.searchUser(UserInfo.getName());
Test.stopTest();
Assert.areEqual(UserInfo.getId(), results[0].Id);
}
- SOSL クエリの単体テストへの追加 | Apex 開発者ガイド | Salesforce Developers
- Test クラス | Apex 開発者ガイド | Salesforce Developers
特定のプロファイルでのテスト実施
特定のプロファイルでテストを実施する場合はSystem.runAs
を使用する。
DML操作をrunAsブロックで囲むことで、混合DMLエラーを回避することができる。
指定するユーザーをinsertする必要はなく、インスタンスを渡すだけで良い。ユーザーのinsertは時間がかかるので可能なら避ける。
System.runAs
はユーザーライセンスの制限を無視するため、ユーザライセンスが足らない場合でも使用できる。
@IsTest
public class TestDataFactory {
public static User createSalesUser() {
// 乱数作成は後述
String randToken = generateRandomString(5);
User obj = new User();
obj.Email = randToken + '@my-company-name.example.com';
// 仮にinsertが必要な時、すでに存在するユーザー名は失敗になるため、乱数を使用する。
obj.UserName = obj.Email + generateRandomString(10);
obj.LastName = randToken;
obj.Alias = randToken;
obj.ProfileId = querySalesProfileId();
obj.EmailEncodingKey = 'UTF-8';
obj.LanguageLocaleKey = 'ja';
obj.LocaleSidKey = 'ja_JP';
obj.TimeZoneSidKey = 'Asia/Tokyo';
return obj;
}
}
@IsTest
static void querySettingTest() {
User salesUser = TestDataFactory.createSalesUser();
// runAsの引数はinsert不要
System.runAs(salesUser) {
Test.startTest();
String result = SampleService.doSomething();
Test.stopTest();
Assert.areEqual('sample', result, 'テスト結果');
}
}
テストユーティル
ダミーのレコードID
DML操作はコストが高い。テスト対象のメソッドの関心事がDMLでない場合、ダミーレコードを使用してテストに高速するとよい。
private static Integer fakeIdNum = 1;
/**
* @description ダミーのSalesforceIDを生成する。
* @param token 生成するIDのオブジェクトタイプ
* @return ダミーのSalesforceID
*/
public static Id generateFakeId(SObjectType token) {
String index = String.valueOf(fakeIdNum++);
String prefix = token.getDescribe().getKeyPrefix();
String padding = '0'.repeat(12 - index.length());
return (Id) (prefix + padding + index);
}
Case c = new Case(Id = TestUtil.generateFakeId, Status = 'New');
乱数
/**
* @description 乱数生成メソッド(半角英数)
* @param len 文字列の長さ
* @return 乱数
*/
public static String generateRandomString(Integer len) {
String chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + '0123456789' + 'abcdefghijklmnopqrstuvwxyz';
String randStr = '';
while (randStr.length() < len) {
Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), chars.length());
randStr += chars.substring(idx, idx + 1);
}
return randStr;
}
カスタム設定登録
テストクラスから既存のカスタム設定にはアクセスできないため、カスタム設定を参照する処理をテストする場合、カスタム設定を登録する必要がある。
SampleSetting__c setting = SampleSetting__c.getOrgDefaults();
setting.BoxUserName__c = 'qiita@example.com';
upsert setting;
権限セット付与
テストクラスで作成したユーザーに権限セットを付与してrunAsする場合に使用する。
PermissionSetAssignmentオブジェクトは設定オブジェクトなので注意が必要。
/** 既存の権限セットを付与する */
public static void assignPermissionset(Id userId, String permissionSetName) {
PermissionSet ps = [SELECT Id FROM PermissionSet WHERE Name = :permissionSetName];
insert new PermissionSetAssignment(AssigneeId = userId, PermissionSetId = ps.Id);
}
条件ベース共有ルールはApexテストだと使用できない。そのため、全て参照権限を付与してアクセスする場合は下記のようにする(他にSharingレコードを作成する方法もある)。
/** テストクラスで権限セットを作成して付与する */
public static void assignViewAllPermission(Id userId, String objectName) {
String name = 'ApexTest' + generateRandomString(20);
PermissionSet ps = new PermissionSet(Name = name, Label = name);
insert ps;
insert new ObjectPermissions(
ParentId = ps.Id,
SobjectType = objectName,
PermissionsRead = true,
PermissionsViewAllRecords = true // すべて参照
);
insert new PermissionSetAssignment(AssigneeId = userId, PermissionSetId = ps.Id);
}