9
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Apexテストクラス チートシート

Last updated at Posted at 2023-09-26

テストクラスの基本

テストクラス

/**
 * @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;
    }
}

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();
    // 略
}

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);
}

特定のプロファイルでのテスト実施

特定のプロファイルでテストを実施する場合は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);
}

参考

9
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?