Salesforce Apexテストクラスの完全マスターガイド:開発者向けベストプラクティス

Salesforce 開発者として、高品質で信頼性の高いコードを提供することは私たちの最も重要な責務の一つです。その中核をなすのが、Apex Test Classes (Apexテストクラス) の作成です。テストクラスは、単に Salesforce が要求する Code Coverage (コードカバレッジ) を満たすためだけのものではありません。これは、アプリケーションのロジックが期待どおりに機能することを保証し、将来の変更による意図しない副作用(リグレッション)を防ぎ、システムの保守性を高めるための不可欠なプロセスです。この記事では、Apexテストクラスの基本からベストプラクティスまでを包括的に解説します。


背景と応用シナリオ

Salesforce Platform では、本番環境に Apex コード(トリガやクラス)をデプロイする際に、全体の 75% 以上のコードカバレッジを達成することが義務付けられています。これは、プラットフォーム全体の安定性と信頼性を維持するための重要な品質ゲートです。

しかし、テストの価値はカバレッジの数値を満たすこと以上にあります。具体的な応用シナリオは多岐にわたります:

  • ビジネスロジックの検証: カスタムの計算ロジック、入力データに対する検証ルール、複雑な条件分岐が正しく動作することを確認します。
  • トリガの動作確認: レコードの作成、更新、削除時にトリガが意図したとおりに実行され、関連レコードが正しく変更されるかをテストします。特に、一度に複数のレコード(バルク処理)が処理されるケースを検証することは非常に重要です。
  • 非同期処理のテスト: Batch Apex、Queueable Apex、Future メソッドといった非同期処理が、ガバナ制限内で正常に完了するかを検証します。
  • 権限と共有のテスト: 特定のプロファイルや権限セットを持つユーザとしてコードを実行し、オブジェクトや項目レベルのセキュリティが正しく適用されているかを確認します。
  • 例外処理の検証: 無効なデータが入力された場合や、予期せぬエラーが発生した場合に、コードが適切に例外を処理し、システムを不安定にしないことを保証します。

これらのシナリオを網羅する堅牢なテストクラスを作成することで、自信を持ってコードをリリースし、長期的に安定したシステムを構築することが可能になります。


原理説明

Apex テストの動作原理を理解することは、効果的なテストクラスを作成するための第一歩です。主要な概念を以下に示します。

@isTest アノテーション

テストクラスは @isTest アノテーションをクラス定義の前に付けることで宣言されます。このアノテーションが付与されたクラスは、組織の Apex コードサイズの上限にはカウントされません。同様に、テストメソッドも @isTest をメソッド定義の前に付けます。

@isTest
private class MyTestClass {
    @isTest static void myTestMethod() {
        // テストロジック
    }
}

データ分離とテストデータ

テストメソッドは、デフォルトでは組織の既存データにアクセスできません。これは、テストが特定のデータに依存することなく、どんな環境でも一貫して実行できるようにするためです。そのため、テストメソッド内では、テストに必要なすべてのレコード(取引先、商談など)をプログラムで作成する必要があります。これにより、テストの独立性と再現性が保証されます。

例外的に @isTest(SeeAllData=true) を使用すると組織データにアクセスできますが、これはベストプラクティスとは見なされず、テストが特定の環境に依存してしまうため、可能な限り避けるべきです。

Test.startTest() と Test.stopTest()

これらのメソッドは、テストコードの特定の部分を分離するために使用されます。Test.startTest()Test.stopTest() の間に記述されたコードは、新しい、独立した Governor Limits (ガバナ制限) のセットを取得します。これは、テストの準備コード(データ作成など)が、テスト対象のコードのガバナ制限に影響を与えないようにするために非常に重要です。

また、このブロック内で呼び出された非同期 Apex (Future, Queueable, Batch) は、Test.stopTest() が実行された時点で同期的に実行が完了するため、非同期処理のテストには不可欠です。

Assertions (アサーション)

テストの目的は、コードを実行するだけでなく、その結果が正しいことを検証することです。Assertions (アサーション) は、コードの出力が期待値と一致するかどうかをチェックするステートメントです。System.assertEquals(expected, actual)System.assertNotEquals(unexpected, actual)System.assert(condition) などを使用します。アサーションのないテストは、コードがエラーなく実行されたことを示すだけで、ロジックの正しさを保証するものではありません。


示例代码

ここでは、Salesforce 公式ドキュメントにある日付を検証する簡単なユーティリティクラス VerifyDate と、そのテストクラス TestVerifyDate を例に解説します。

テスト対象クラス: VerifyDate.apxc

このクラスは、指定された月と年の日数が正しいかどうかを検証するメソッドを提供します。

public class VerifyDate {
    public static Date CheckDates(Integer year, Integer month, Integer day) {
        try {
            Date dt = Date.newInstance(year, month, day);
            return dt;
        } catch (System.DateException e) {
            // DateException が発生した場合、カスタム例外をスローする
            throw new DateException('Invalid date.');
        }
    }

    public class DateException extends Exception {}
}

テストクラス: TestVerifyDate.apxc

このテストクラスは、VerifyDate.CheckDates メソッドの正常系(有効な日付)と異常系(無効な日付)の両方のシナリオをテストします。

@isTest
private class TestVerifyDate {

    // 正常系のテストメソッド
    @isTest static void testValidDate() {
        Date resultDate;
        // Test.startTest() と Test.stopTest() を使用してテストのコンテキストを分離
        Test.startTest();
        // 2013年2月28日という有効な日付を渡す
        resultDate = VerifyDate.CheckDates(2013, 2, 28);
        Test.stopTest();
        
        // アサーション:返された日付がnullでないことを確認
        System.assert(resultDate != null, 'Valid date should not return null');
        // アサーション:返された日付の各要素が期待通りであることを確認
        System.assertEquals(2013, resultDate.year(), 'Year should be 2013');
        System.assertEquals(2, resultDate.month(), 'Month should be 2');
        System.assertEquals(28, resultDate.day(), 'Day should be 28');
    }

    // 異常系のテストメソッド
    @isTest static void testInvalidDate() {
        try {
            Test.startTest();
            // 2013年2月29日という無効な日付を渡す (2013年はうるう年ではない)
            VerifyDate.CheckDates(2013, 2, 29);
            Test.stopTest();
            // 例外がスローされるべきなので、この行に到達した場合はテスト失敗
            System.assert(false, 'DateException should have been thrown');
        } catch (VerifyDate.DateException e) {
            // 期待通りにカスタム例外がキャッチされたことを確認
            // ここでアサーションを行うことで、正しい例外がスローされたことを検証
            System.assert(e.getMessage().contains('Invalid date.'), 'Exception message is not as expected');
        } catch (Exception e) {
            // 予期しない他の例外が発生した場合はテスト失敗
            System.assert(false, 'An unexpected exception type was thrown: ' + e.getTypeName());
        }
    }
}

この例では、正常系と異常系の両方をテストしています。testValidDate メソッドでは、有効な日付が正しく処理されることをアサーションで確認します。一方、testInvalidDate メソッドでは、無効な日付が与えられたときに、意図した通りの DateException がスローされることを try-catch ブロックで検証しています。これが堅牢なテストの基本です。


注意事項

ガバナ制限 (Governor Limits)

テストも本番コードと同じガバナ制限の対象となりますが、Test.startTest()Test.stopTest() を使うことで、テスト対象のコードにクリーンな制限セットを提供できます。特に、大量のテストデータを作成した後に本処理を呼び出す場合は必須です。

テストデータの管理

テストデータ作成はテストクラスの大部分を占めることがあります。これを効率化するために、@TestSetup アノテーション付きのメソッドを使用することが推奨されます。このメソッドはクラス内の各テストメソッドが実行される前に一度だけ実行され、作成されたデータは各テストメソッドで利用可能になります(ただし、各テストメソッドは独立したトランザクションで実行されるため、あるテストメソッドでのデータ変更は他のテストメソッドに影響しません)。

@isTest
private class MyControllerTest {
    @TestSetup
    static void makeData(){
        // すべてのテストメソッドで共通して使用するデータを作成
        Account a = new Account(Name = 'Test Account');
        insert a;
    }

    @isTest static void testMethod1() {
        // @TestSetup で作成されたデータにアクセス可能
        Account testAcc = [SELECT Id, Name FROM Account WHERE Name = 'Test Account' LIMIT 1];
        System.assert(testAcc != null);
    }
}

より複雑なデータ構造が必要な場合は、テストデータを作成するための再利用可能なクラス(Test Data Factory)を設計することも一般的です。

ユーザコンテキストでのテスト

コードが特定のプロファイルや権限を持つユーザの下でどのように動作するかをテストするには、System.runAs(user) ブロックを使用します。これにより、レコードの作成や更新が、指定したユーザの権限(項目レベルセキュリティや共有ルール)に従って実行されることをシミュレートできます。

@isTest static void testAsStandardUser() {
    // テスト用の標準ユーザを作成
    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');

    System.runAs(u) {
        // このブロック内のコードは、ユーザ 'u' として実行される
        // 例: 権限のないオブジェクトへのアクセスが期待通り失敗するかをテスト
        try {
            Contact c = new Contact(LastName = 'Test');
            insert c; // 権限によってはここでエラーが発生する
        } catch(Exception e) {
            // 例外が期待通り発生したことを検証
            System.assert(e.getMessage().contains('insufficient access rights'));
        }
    }
}

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

高品質な Apex テストクラスを作成することは、Salesforce 開発者にとって不可欠なスキルです。最後に、重要なベストプラクティスをまとめます。

  1. カバレッジだけでなく品質を追求する: 75%は最低ラインです。重要なのは、コードの振る舞いを検証する意味のあるアサーションを記述することです。
  2. 一つのテストメソッドでは一つのことをテストする: テストメソッドには、その目的が明確にわかる名前を付け(例:testAccountCreationWithValidData)、単一の機能やシナリオに焦点を当てます。
  3. - ポジティブシナリオとネガティブシナリオをテストする: コードが正常に動作するケースだけでなく、エラーや例外が発生するケースも必ずテストします。
  4. バルク処理をテストする: Salesforce では、データは常に複数レコード単位で処理される可能性があります。トリガなどをテストする際は、必ずリスト(例:200レコード)を使ってテストし、ガバナ制限に抵触しないことを確認します。
  5. @TestSetup を活用する: 共通のテストデータを効率的に作成し、テストの実行時間を短縮します。
  6. SeeAllData=true は使用しない: テストの独立性と再現性を保つため、原則として使用を避けます。
  7. System.runAs() で権限をテストする: セキュリティと共有ルールが正しく機能することを確認します。

これらの原則に従うことで、あなたは単なる「カバレッジを満たすコード」ではなく、ビジネスの要求に確実に応え、将来の変更にも強い、真に価値のあるコードを書くことができる Salesforce 開発者になることができるでしょう。

コメント