未経験から1ヶ月間で学んだApexテストクラスの基礎知識

文系で未経験の私が業務を通して1ヶ月間テストクラスを作成してみて学んだ基礎知識をご紹介します。

Salesforce専用のプログラム言語であるApexをリリースまたは AppExchange 用にパッケージ化する際にはテストクラスを作成する必要があります。

本記事では、文系で未経験の私が業務を通して1ヶ月間テストクラスを作成してみて学んだ基礎知識をご紹介します。

Apexテストクラスとは

Apexテストクラスは単体テストで使用されます。
テストクラスを作成することで、Apex クラスやトリガーが想定どおりに動作することを確認し、品質の高いコードをリリースすることができます。

そもそもSalesforceでは、Apexコードが単体テストでカバー率75%を超えている必要があり、下回る場合はリリースができない仕様となっています。(トリガーは1%以上のカバー率があること)
コードカバー率はSalesforce上で確認可能です。開発環境であるVisual Studio Code上で確認することも可能です。

Apexテストクラスの考慮事項
・「System.debug」はApexコードカバー率の対象外です。
・テストメソッド間ではトランザクションは保持されません。
・Apexは本番組織では記述できません。※開発可能な組織
・すべてのクラスとトリガーが正常にコンパイルされます。

startTest() と stopTest()を使用するメリット

テストクラスを作成するにあたって大事になるのが、Test.startTest()Test.stopTest()です。
2つのメリットについてご紹介します。

①ガバナ制限の範囲を限定してテストをする

Salesforceではマルチテナンシーの概念を採用しているため、共有リソースを独占しないようガバナ制限をかけており、Apex コードにおいても適応されています。
ガバナ制限」とは、Salesforceリソースの使用制限であり、各トランザクションが利用できるリソース(例えば、SOQLクエリの数、DML操作の数、メモリ使用量など)が制約されています。
テストコード実装時に、テストメソッドの中でstartTest()をコールすると、その時点でガバナ制限がリセットされ、後続のテストコードは新しいトランザクションとして扱われます。
これにより、テスト環境でのリソース使用が独立して測定され、実際の動作に近い状況でテストが実行できます。

②非同期プロセスを同期して実行する

非同期プロセスでは、ユーザーがタスクの終了まで待たずしてタスクをバックグラウンドで実行します。
そのため、非同期プロセスのテストを実行する場合、最後まで処理が終了しないままアサーションで処理の結果を確認してしまい正しいテストが行われません。
しかし、startTestメソッド後に実行された非同期コールはシステム的に収集され、stopTestメソッドを実行すると収集されたプロセスが同期して実行されるようになります。

Apexアノテーション

Apex アノテーションは、メソッドまたはクラスの使用方法を定義するものです。
今回はその中でも、使用頻度が高いと感じたアノテーションを3つご紹介いたします。

@IsTest

@IsTestは、テストコードであることを認識させたい際に使用します。
メソッドに定義すると、メソッドをRun Test (テストを実行) することができます。

【 @IsTest を定義する際の考慮事項 】
・クラスとメソッドは private または public にする必要があります。
・最上位クラス(外部クラス)である必要があります。
・@IsTestをクラスに定義すると、デフォルトの Apex コード文字制限の 6 MBに含まれません。
※Apex コード文字制限 : Apexコード実行時(1つのトランザクション)に使用されるメモリ(ヒープ)に制限があり、同期トランザクションで6MB、非同期トランザクションでは12MBに設定されています。

@TestSetup

@TestSetupは他のテストメソッドより前に実行され、テストデータを作成する際に使用します。
作成したテストデータは、テストクラス内の各メソッドでSOQL取得し使用できます。

@TestSetupを使用せず、各メソッドごとにテストデータを作成することも可能ですが、テスト実行時間の短縮やテストコードの簡素化として最適です。

【@TestSetupを定義する際の考慮事項】
・作成したテストデータはデーターベースには保存されません。
・テストクラスごとに1 つのみ定義できます。
・@isTest(SeeAllData=true) をテストクラスまたはテストメソッドに定義している場合、@TestSetupはサポートされません。

@TestVisible

@TestVisibleは、テストクラス外にある別のクラスの「private」または「protected」(アクセス修飾子)のメンバ変数、メソッド、内部クラスにアクセスしたい場合に使用します。
それにより、アクセス修飾子を「public」に変更することなく、アクセスが可能になります。

テストデータを作成することができない(環境のデータに依存する)カスタムメタデータ型の値をテストクラス内で変更したい際などに使用されます。

3つのアノテーションを使用したサンプルコード

public class AccountUpdate {
    
    // テストクラスで使用するため、変数に@TestVisibleを定義
    @TestVisible
    private static String testVisibleVariable = 'System Update';

    // 取引先を更新するメソッド
    public static void updateAccount(String accountId, String newName) {
        // 取引先を取得
        Account acc = [SELECT Id, Name FROM Account WHERE Id = :accountId LIMIT 1];

        // 取引先が存在する場合に更新する
        if(acc != null) {
            // @TestVisible変数を使用して取引先情報を更新
            acc.Name = testVisibleVariable + ' ' + newName;
            update acc;
        }
    }
}
AccountUpdate.cls
@IsTest
private class AccountUpdateTest {

    // テストデータを作成するメソッド
    @TestSetup
    static void createTestData() {
        Account acc = new Account(Name='Test Account');
        insert acc;
    }

    // 取引先の更新をテストするメソッド
    @IsTest
    static void updateAccountTest() {
        // @TestSetupで作成した取引先をSOQLで取得
        Account acc = [SELECT Id, Name FROM Account WHERE Name ='Test Account' LIMIT 1];
        String accId = acc.Id;
        String newName = 'New Name';

        // 取引先を更新
        Test.startTest();
        AccountUpdateClass.updateAccount(accId, newName);
        Test.stopTest();

        // 更新された取引先を取得
        Account updatedAccount = [SELECT Id, Name FROM Account WHERE Id = :accId LIMIT 1];

        // @TestVisible変数がテストクラスでも使用できるのか確認
        Assert.areEqual(AccountUpdateClass.testVisibleVariable, 'System Update','同一ではありません');
    }
}
AccountUpdateTest.cls

System.runAs で実行ユーザーを指定する

Apexコードはシステムモードで実行されます。そのため、ユーザーの権限とレコード共有は考慮されずにテストが行われます。
しかし、runAsメソッドを使用することにより、テストクラス内で作成したユーザーや既存ユーザーを使用してテストを実行することができます。つまり、指定したユーザー権限と項目レベル権限を適応したテストが実施できます。
@isTest
private class runAsTest {
    @isTest 
    static void testUserProcess() {
        // 営業ユーザーを新規作成する
        String uniqueUserName = 'standarduser' + DateTime.now().getTime() + '@example.com';
        Profile sales = [SELECT Id FROM Profile WHERE Name='営業'];
        User testUser = new User(Alias = 'test', 
        Email='test@sample.com',
        EmailEncodingKey='UTF-8', 
        LastName='TestUser', 
        LanguageLocaleKey='ja',
        LocaleSidKey='ja_JP', 
        ProfileId = sales.Id,
        TimeZoneSidKey='Asia/Tokyo',
        UserName=uniqueUserName);
         
         System.runAs(testUser) {
              // 営業ユーザーで実行する
              Account acc = new Account(Name='Test Account');
              insert acc;
              // 営業プロファイルのIdをデバックする
              System.debug(UserInfo.getProfileId());
          }
    }
}
runAsTest.cls

Assertで結果を比較する

テストを実施する上で、アサーション (assertion) は重要な役割を持っています。
アサーションとは、期待する結果に対して実際の結果が一致するのかを比較するためのものです。
両者が一致する場合はTRUEを返し、失敗する場合はテスト失敗に終わります。
これにより、コードが適切に動作することを証明できます。

Apexアサーションには、SystemクラスとAssertクラスの大きく2つがあります。
上記2つの違いについては、下記の記事をご参照ください。
System.assertEqualsはもう古い?

Salesforceは「Assertクラス」を使用することを推奨しています。
今回は、使用頻度が高いと感じたAssertクラスのメソッドを2つご紹介します。
その他のメソッドはこちらをご参照ください。

① Assert.areEqual(expected, actual, msg)

引数の「expected」と「actual」が同一であることを証明します。
結果が一致しない場合はエラーを返しコードの実行を停止します。そして、「msg」で指定したメッセージを表示させます。

② Assert.areNotEqual(notExpected, actual, msg)

引数の「expected」と「actual」が同一ではないことを証明します。
結果が一致する場合はエラーを返しコードの実行を停止します。そして、「msg」で指定したメッセージを表示させます。

おわりに

今回は、Apexテストクラスの基礎知識についてご紹介しました。
Apexをリリースまたは AppExchange 用にパッケージ化する際にはテストクラスが必要であり、カバレッジ75%を超えている必要があります。
しかし、カバレッジの100%を目指すだけではなく、ポジティブとネガティブのテスト、実行ユーザーを変更したテストなど、さまざまなテストケースでテストしアサーションで結果比較することで、品質の高いコードをリリースすることが求められます。

さまざまなテストパターンを考慮した簡素化したテストコードを作成していけるよう、本記事が参考になりましたら幸いです!