コールアウトがあるバッチの単体テストクラス作成に、Apex初心者が挑んだ話 - 基本編

こんにちは、 Takahiroです。 Appirioに入社して早いもので1年が経ちました。
コロナ禍対応によりリモートワークになってからというもの、以前よりも近所に足を運ぶ機会が増え、オシャレなクラフトビール屋さんを発見したり、とても美味なベリー&チョコレートデニッシュを発見したりと、近所での新発見が多い今日この頃です。
今回は、業務を通して、コールアウトがあるバッチの単体テストクラスを書く機会がありましたので、その中で得た知識を2回に渡り掲載します。ちなみに、Apexで初めて単体テストクラスを書きました。 

1回目は、単体テストクラスの基本についてご紹介します。
2回目は、コールアウトがあるバッチの単体テストクラスについてご紹介します。

1. 単体テストクラスを書き始める前に

Salesforceの仕組みとして、sandboxから本番環境へコードをリリースするためには、テストクラスによってコードカバレッジ(コードカバー率、網羅率)を最低でも75%を満たす必要があります。コードカバレッジが75%未満であると、リリースできません。そのため、テストクラスによるコードのテストは、必須のものになります。

私はJava出身で、コードカバレッジは100%を目指してテストクラスを作成していましたが、Salesforceの仕組みの様にコードカバレッジを満たさないとリリースできないという仕組みはありませんでした。そのため、Salesforceでプログラムを書いたら、「完成したからリリースだ!あれ、コードカバレッジが足りていないからリリースできない!」といった状態にならないように注意したいポイントです。

一般的にテストクラスは、単体テスト実施する目的で記述される場合が多いですが、ここでは、単体テストを目的としたクラスは「単体テストクラス」と表記し、単に「テストクラス」とした場合は、Salesforceにおけるテストクラスを指すものとします。

2. 単体テストクラスはなぜ必要なのか

そもそもどうして単体テストが必要なのでしょうか?
プログラムを直接実行したり、画面から操作したりした後に、生成や更新されたデータを直接確認すれば確かにテストはできますが、私は以下の理由で単体テストが必要だと考えています。

  • コード上のバグをテストクラスで潰しておくことで、後の結合テストや総合テストで発見されるバグの発生源の特定が容易になります。個人的には、テストクラスにより、コード上のバグのほとんどが発見でき、また、考慮事項の漏れなども発見できています
  • コードカバレッジと組み合わせることで、条件分岐があるプログラムでもテストパターンが漏れることなくテストが行え、プログラム上で、テスト対象が期待している値になっているかをテストできます
  • 同じテストデータで何回でも同じテストができるので、後にプログラムの変更があった場合でも、既存機能に影響がないことを保障できます(画面上などからのレグレッションテストは別途行う)
3. 単体テストクラスの作成方法

まずは、基本となる単体テストクラスを作成してみて、Apexでの単体テストクラスの作成方法を学びました。
以下のクラスを例に、このクラスに対しての単体テストクラスを作成してみます。

/** 

 * サンプルクラス 

 */ 

public with sharing class SampleAccountClass { 

    public Account acc {getprivate set;} 

 

    /** 

     * 取引先名を引数に、取引先のインスタンスを作成する 

     * @param accountName 取引先名 

     */ 

    public SampleAccountClass(String accountName) { 

        acc = new Account(); 

        acc.Name = accountName; 

    } 

 

    /** 

     * 取引先を作成する 

     */ 

    public void save() { 

        insert acc; 

    } 

} 

単体テストクラスの名前は、何でも良いのですが、[テスト対象クラス名]Testのような形が分かりやすくオススメです。単体テストクラスは、以下になります。

/** 

 * サンプルテストクラス 

 */ 

@isTest 

private class SampleAccountClassTest { 

 

    @TestSetup 

    static void makeData() { 

        // ユーザ作成 

        UserRole testRole = new UserRole(Name = 'testRole'); 

        insert testRole; 

 

        Profile p = [SELECT Id FROM Profile WHERE Name = 'システム管理者' LIMIT 1]; 

        User testUser = new User( 

                        alias = 'test', 

                        email = 'test@hogehoge.hogehoge', 

                        emailEncodingKey = 'UTF-8', 

                        lastName = 'test', 

                        languageLocaleKey = 'ja', 

                        localeSidKey = 'ja_JP', 

                        profileId = p.Id, 

                        userRoleId = testRole.id, 

                        timeZoneSidKey = 'Asia/Tokyo', 

                        userName = 'test@hogehoge.hogehoge'); 

        insert testUser; 

    } 

 

    /** 

     * Accountが作成されること 

     */ 

    @isTest 

    private static void testCase001() { 

        // 指定したユーザで実行 

        User u = [SELECT id FROM User WHERE userName = 'test@hogehoge.hogehoge' LIMIT 1]; 

        System.runAs(u) { 

            // Test.startTestとTest.stopTestの間に実際の処理を実行する 

            Test.startTest(); 

            SampleAccountClass sample = new SampleAccountClass('test company'); 

            sample.save(); 

            Test.stopTest(); 

        } 

 

        // 期待値通りであることをテストする 

        Account actualAccount = [SELECT idname FROM Account WHERE name = 'test company']; 

        System.assertEquals('test company'actualAccount.Name); 

    } 

} 

  • @isTest
    • クラスに対して使用して、テストクラスであることを示します。テストクラスは、コードカバレッジの対象外となるので、テストクラスのためのテストクラスは作成する必要はありません。
    • メソッドに対して使用することで、テストメソッドであることを示します。テストメソッド内の処理が問題なく最後まで実行された場合、このテストケースは、成功となります。

isTest アノテーション | Apex 開発者ガイド | Salesforce Developers

  • Test.startTest()とTest.stopTest()

Test.startとTest.stopの間では、新たなガバナ制限の割当で実行されます。Test.startとTest.stopの間で実際のプログラムの処理を実行し、ガバナ制限に当たらないことをテストできます。

Limits、startTest、および stopTest の使用 | Apex 開発者ガイド | Salesforce Developers

  • System.runAs(testUser)

テストメソッド内でrunAsブロックを使用すると、ブロック内で囲った部分は、指定したユーザで実行されます。例えば、runAsブロックを使用しないでレコードを作成した場合は、テストを実行したユーザでそのレコードは作成されますが、runAsブロック内でレコードを作成すると、runAsブロックで指定したユーザでレコードを作成することができます。レコードの共有を考慮したテストを行いたい場合に使用できます。(Knowledgeについては他のオブジェクトと違い、例えば、runAsブロック内でKnowledgeを作成しようとした際に、runAsブロックで指定したユーザの権限にKnowledgeの作成権限がないと作成できません。頭の片隅に置いておく程度で大丈夫です。)

また、runAsブロックは、混合DMLを回避するために使用されます。例えば、同トランザクション内でUserのロールと取引先を更新しようとすると発生します。混合DMLが必要な場面には時々遭遇しますので、その時は、runAsブロックを使用して回避しましょう。

今回のテストメソッドでは、使用する必要はありませんが、今後の拡張でテストクラスやテストメソッドに変更があることを考え、runAsブロックで囲っておきました。

Test.startとTest.stopの間では、新たなガバナ制限の割当で実行されます。Test.startとTest.stopの間で実際のプログラムの処理を実行し、ガバナ制限に当たらないことをテストできます。

runAs メソッドの使用 | Apex 開発者ガイド | Salesforce Developers

テストメソッドでの混合 DML 操作 | Apex 開発者ガイド | Salesforce Developers

DML 操作で同時に使用できない sObject | Apex 開発者ガイド | Salesforce Developers

  • System.assertEquals(expected, actual)

期待した値と実際の値を比較して、プログラムが期待通りに動いているかをテストできます。expectedとactualの引数が一致しない場合、System.AssertExceptionが発生し、テストは失敗となります。2つの引数が一致している場合、例外は発生しません。

System クラス | Apex 開発者ガイド | Salesforce Developers

  • @TestSetup

@TestSetupが付いたメソッドは、各テストメソッドが実行される前に呼び出されます。各テストメソッドで共通した初期テストデータやカスタムメタデータなどが必要な場合に適しています。

@isTest 

private class SampleAccountClassTest { 

    /** 

     * 各テストケースが実行される前に呼び出される 

     */ 

    @TestSetup 

    private static void init() { 

        // 初期データを作成したり、カスタムメタデータ設定などをしたりする 

    } 

 

    @isTest 

    private static void testCase001() { 

        // 何かテスト 

    } 

 

    @isTest 

    private static void testCase002() { 

        // 何かテスト 

    } 

} 

TestSetup アノテーション | Apex 開発者ガイド | Salesforce Developers

テスト設定メソッドの使用 | Apex 開発者ガイド | Salesforce Developers

4. 単体テストクラスの作成方法

テストクラスの実行方法とコードカバレッジを確認する方法を、開発者コンソールとVisual Studio Code(VSCode)でそれぞれ記載します。私は、VSCodeでテストクラスを実行する方が多いです。 

4.1. 開発者コンソール
4.1.1. 開発者コンソールでテストを実行する

[Test] -> [New Run] を選択します。

グラフィカル ユーザー インターフェイス, アプリケーション

自動的に生成された説明

実行したいテストクラスを選択し、実行したいテストメソッドにチェックを付けます。[Run]を押すとテストが実行されます。

グラフィカル ユーザー インターフェイス

自動的に生成された説明

テストの実行結果は、開発者コンソール画面下の[Tests]のタブで確認できます。

[Status]の列が緑色のチェックの場合、テストが成功したことを意味します。赤色のバツが付いた場合、テストが失敗したことを意味します。

グラフィカル ユーザー インターフェイス, テキスト, アプリケーション

自動的に生成された説明

4.1.2. 開発者コンソールでコードカバレッジを確認する

開発者コンソール画面右下の[Overall Code Coverage]から対象のクラスを選択すると、コードカバレッジの詳細を見ることができます。[Percent]の列は、対象クラスのコードカバー率を表示しています。青色の箇所は、その部分がテストクラスにより実行されたことを意味します。