Salesforce Apexテストクラス完全ガイド:開発者のためのコードカバレッジと品質向上の秘訣

背景と応用シナリオ

Salesforce開発者として、私たちは日々、ビジネスロジックを実装するためにApexコードを書いています。しかし、コードを書くこと自体はプロセスの半分に過ぎません。そのコードが期待通りに動作し、将来の変更によって意図せず壊れることがないように保証すること、すなわち「品質の担保」がもう半分の重要なプロセスです。ここで中心的な役割を果たすのが Apex Test Classes (Apexテストクラス) です。

Salesforceプラットフォームには、本番環境にApexコードをデプロイするための厳格な要件があります。その最も有名なものが、Code Coverage (コードカバレッジ) が75%以上でなければならないというルールです。これは、テストによって実行されるApexコードの行数が、全体の行数の少なくとも75%を占める必要があることを意味します。

しかし、この75%という数値を単なる「デプロイのためのノルマ」と捉えるべきではありません。Apexテストクラスの真の価値は、以下の点にあります。

  • 品質保証: 作成したコードが、様々な条件下で正しく動作することを検証します。
  • リグレッション防止: 既存のコードを修正した際に、他の機能に悪影響(リグレッション)が出ていないことを確認できます。
  • 仕様の文書化: テストコード自体が、そのApexクラスがどのように動作すべきかを示す「生きたドキュメント」の役割を果たします。
  • 保守性の向上: よくテストされたコードは、安心してリファクタリングや機能追加を行うことができます。

応用シナリオは多岐にわたります。Triggerが特定の条件下で正しくレコードを更新するか、Visualforce ControllerやLightning ComponentのApex Controllerが正しいデータを返すか、Batch Apexが大量のデータを正しく処理するか、外部システム連携時のエラーハンドリングは適切か、など、Apexコードが関わるすべてのロジックがテストの対象となります。


原理説明

Apexテストクラスは、本番のデータに影響を与えることなくコードの動作を検証するための特別なApexクラスです。その仕組みを理解するために、いくつかの重要な概念とAnnotation (アノテーション) を見ていきましょう。

@isTest アノテーション

テストクラスやテストメソッドを定義するための基本となるアノテーションです。

  • @isTest class MyTestClass { ... } : クラス全体をテストクラスとして定義します。テストクラスは組織のApexコードサイズの上限(6MB)にはカウントされません。
  • @isTest static void myTestMethod() { ... } : クラス内の特定のメソッドをテストメソッドとして定義します。テストメソッドは必ず static であり、戻り値は void です。

データ分離 (Data Isolation)

デフォルトでは、テストメソッドは組織の既存データにアクセスできません。これはテストの独立性を保ち、本番データに意図せず影響を与えてしまう事故を防ぐための重要な原則です。テストメソッド内で必要なデータは、すべてそのメソッド内(または @testSetup メソッド内)で作成する必要があります。例外的に組織データへのアクセスが必要な場合は @isTest(SeeAllData=true) を使用できますが、これは強く非推奨とされており、テストの移植性や信頼性を損なうため、極力避けるべきです。

Test.startTest() と Test.stopTest()

これはテストメソッド内で非常に重要なペアメソッドです。このブロックで囲まれたコードは、新しい、独立した Governor Limits (ガバナ制限) のセット内で実行されます。

  • Test.startTest(): ガバナ制限のカウンターをリセットし、テストの「本題」がここから始まることを示します。
  • Test.stopTest(): ブロックの終了を示します。このメソッドが呼ばれると、startTest() 以降に実行された非同期処理(Futureメソッド、Queueable Apex、Batch Apexなど)が同期的に実行されます。これにより、非同期処理の結果をテストメソッド内で検証することが可能になります。

@testSetup メソッド

テストクラス内で @testSetup アノテーションを付与されたメソッドは、そのクラス内の各テストメソッドが実行される前に一度だけ実行されます。複数のテストメソッドで共通して使用するテストデータ(例:取引先、商品、カスタム設定など)を作成するのに最適です。これにより、テストデータの作成処理を共通化でき、テスト全体の実行時間を短縮する効果があります。

アサーション (Assertions)

テストの心臓部です。コードを実行するだけでは、それが「正しい結果」を生んだかどうかは分かりません。Assertions (アサーション) は、コードの実行結果が期待通りであるかを検証するためのものです。System クラスには、以下のようなアサーションメソッドが用意されています。

  • System.assertEquals(expected, actual, message): 2つの値が等しいことを検証します。
  • System.assertNotEquals(unexpected, actual, message): 2つの値が等しくないことを検証します。
  • System.assert(condition, message): 条件 (condition) が true であることを検証します。

アサーションのないテストは、コードがエラーなく実行されたことを確認するだけであり、ロジックの正しさを保証するものではありません。質の高いテストには、適切なアサーションが不可欠です。


サンプルコード

ここでは、Salesforce公式ドキュメントにある例を参考に、取引先 (Account) が作成されたときに、その説明 (Description) 項目にプレフィックスを付与する単純なトリガーと、そのロジックをテストするクラスを見てみましょう。

1. Apexトリガーハンドラークラス

まず、トリガーから呼び出されるロジックを含むクラスです。

public class AccountManager {
    public static void addContact(List<Account> accounts) {
        List<Contact> contacts = new List<Contact>();
        for (Account a : accounts) {
            Contact c = new Contact();
            c.FirstName = 'Test';
            c.LastName = a.Name;
            c.AccountId = a.Id;
            contacts.add(c);
        }
        if (!contacts.isEmpty()) {
            insert contacts;
        }
    }
}

2. Apexトリガー

上記のハンドラーを呼び出すトリガーです。

trigger AccountTrigger on Account (after insert) {
    if (Trigger.isAfter && Trigger.isInsert) {
        AccountManager.addContact(Trigger.new);
    }
}

3. Apexテストクラス

そして、このトリガーのロジックを検証するためのテストクラスです。

@isTest
private class AccountManagerTest {
    @isTest
    static void testAddContact() {
        // テストデータとして取引先を作成
        Account acct = new Account(Name='Test Account');

        // Test.startTest() と Test.stopTest() のブロック
        // これにより、ガバナ制限がリセットされ、非同期処理が同期的に扱われる
        Test.startTest();
        // DML操作を実行してトリガーを発火させる
        insert acct;
        Test.stopTest();

        // 結果の検証 (アサーション)
        // トリガーによって関連する取引先責任者が作成されたかを確認
        // SOQLクエリで作成されたはずのレコードを検索
        List<Contact> contacts = [SELECT LastName FROM Contact WHERE AccountId = :acct.Id];
        
        // 1件の取引先責任者が作成されているはず
        System.assertEquals(1, contacts.size(), 'A contact should have been created.');
        
        // 作成された取引先責任者のLastNameが取引先のNameと一致するかを確認
        System.assertEquals('Test Account', contacts[0].LastName, 'Contact LastName should match Account Name.');
    }
}

このテストクラスは、以下のことを行っています。

  1. 準備 (Arrange): Account オブジェクトのインスタンスを作成します。
  2. 実行 (Act): insert acct; を実行してトリガーを発火させます。この処理は Test.startTest()/stopTest() ブロック内に配置されています。
  3. 検証 (Assert): トリガーの実行後、期待通りに Contact が作成され、その内容が正しいかを System.assertEquals() を使って検証しています。


注意事項

権限とプロファイル (Permissions and Profiles)

テストはデフォルトでシステム管理者権限で実行されます。しかし、実際のユーザーは異なるプロファイルや権限セットを持っているため、特定のユーザーコンテキストでコードがどのように動作するかをテストすることが重要です。このために System.runAs(user) ブロックを使用します。これにより、特定のユーザーとしてコードブロックを実行し、そのユーザーの項目レベルセキュリティやオブジェクト権限が正しく適用されるかを確認できます。

@isTest
static void testAsUser() {
    // テスト用のプロファイルとユーザーを作成
    Profile p = [SELECT Id FROM Profile WHERE Name='Standard User']; 
    User u = new User(Alias = 'standt', Email='standarduser@testorg.com', 
        EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US', 
        LocaleSidKey='en_US', ProfileId = p.Id, 
        TimeZoneSidKey='America/Los_Angeles', UserName='standarduser@testorg.com');

    // runAsブロック内で実行されるコードは、指定したユーザー'u'のコンテキストで実行される
    System.runAs(u) {
        // ここに特定のユーザーとしてテストしたいロジックを記述
        // 例:項目レベルセキュリティで参照できない項目にアクセスしようとした場合のエラーをテストするなど
        System.debug('Current User: ' + UserInfo.getUserName());
        System.debug('Current Profile: ' + UserInfo.getProfileId());
    }
}

外部コールアウトのテスト (Testing Callouts)

テストメソッドから直接外部のWebサービスを呼び出すことはできません。これをテストするためには、Mocking (モッキング) という手法を使います。Salesforceは HttpCalloutMock インターフェースを提供しており、これを実装したクラスを作成することで、あたかも外部サービスからレスポンスが返ってきたかのように見せかけることができます。Test.setMock(HttpCalloutMock.class, mockInstance) を使用して、テスト中に使用するモックを設定します。

API制限とガバナ制限

前述の通り、テストコードもガバナ制限の対象となります。特に、SOQLクエリ(100回)、DMLステートメント(150回)などの制限に注意が必要です。テストデータを作成する際にループ内でDMLやSOQLを発行すると、容易に制限に達してしまいます。テストデータはリストにまとめて一括でDML操作を行うなど、常にBulkification (一括処理) を意識する必要があります。


まとめとベストプラクティス

Apexテストクラスは、Salesforce開発の品質を支える根幹です。75%のカバレッジ達成は必須ですが、それをゴールとするのではなく、堅牢で信頼性の高いアプリケーションを構築するための手段と捉えることが重要です。

以下に、効果的なテストクラスを作成するためのベストプラクティスをまとめます。

  • 一つのことをうまくテストする: 1つのテストメソッドでは、1つの機能やシナリオを検証することに集中しましょう。メソッド名を testMethodName_Condition_ExpectedResult のように具体的にすることで、テストの意図が明確になります。
  • 一括処理をテストする: 単一レコードだけでなく、常に複数のレコード(理想は200レコード)を処理するシナリオをテストし、コードがガバナ制限に耐えられることを確認してください。
  • 正常系・異常系・境界値をテストする: コードが期待通りに動く「正常系」だけでなく、必須項目が空の場合や不正なデータが入力された場合の「異常系」のシナリオも必ずテストします。
  • @testSetup を活用する: 共通のテストデータは @testSetup にまとめることで、テストの実行速度を向上させ、コードをクリーンに保ちます。
  • SeeAllData=true は避ける: テストの独立性と再現性を保つため、このオプションは使用しないでください。
  • System.runAs() でユーザーコンテキストをテストする: 異なる権限を持つユーザーでの動作を検証し、セキュリティを担保します。
  • アサーションを必ず記述する: テストの目的はコードを実行することではなく、結果が正しいことを「断言(assert)」することです。意味のあるアサーションを複数記述しましょう。

優れたテストを書くことは、優れたコードを書くことと同義です。これらのプラクティスを日々の開発に取り入れることで、あなたもより信頼性の高いSalesforceアプリケーションを構築できる、優れた開発者になることができるでしょう。

コメント