Skip to main content

Apex 単体テストを始める

学習の目的

この単元を完了すると、次のことができるようになります。

  • Apex 単体テストの主要な利点を説明する。
  • クラスとそのテストメソッドを定義する。
  • クラスのすべてのテストメソッドを実行し、エラーを調べる。
  • 一連のテストクラスを作成し、実行する。
メモ

メモ

このバッジのハンズオン Challenge は日本語、スペイン語 (LATAM)、ポルトガル語 (ブラジル) に対応しています。Playground の言語を変更するには、こちらの指示に従ってください。日本語等、翻訳された言語と英語に差異がある可能性があります。英語以外の言語での指示に従って Challenge に合格できなかった場合は、[Language (言語)] と [Locale (地域)] をそれぞれ [English (英語)]、[United States (アメリカ合衆国)] に切り替えてからもう一度お試しください。

翻訳版の Trailhead を活用する方法については、自分の言語の Trailhead バッジをご覧ください。

Apex 単体テスト

Apex テストフレームワークでは、Lightning プラットフォーム上で Apex クラスとトリガーのテストを記述して実行できます。Apex 単体テストにより、Apex コードの品質が高まり、Apex のリリース要件を満たすことができます。

テストは、長期的な開発を成功に導く鍵であり、開発プロセスにおいて不可欠な要素です。Apex テストフレームワークを使用すれば、Apex コードは簡単にテストできます。Apex コードは、Sandbox 環境または開発者組織でのみ記述でき、本番組織では記述できません。Apex コードは、Sandbox から本番組織にリリースできます。また、アプリケーション開発者は、パッケージを Lightning Platform AppExchange にアップロードすることで、Apex コードを開発者組織から顧客に配布できます。Apex 単体テストは、品質保証にとって不可欠なだけでなく、Apex のリリースと配布の要件でもあります。 

Apex 単体テストには次の利点があります。

  • Apex クラスやトリガーが期待どおりに機能することを確認できる。
  • 一連の回帰テストを用意することで、クラスやトリガーが更新されるたびに再実行でき、将来アプリケーションが更新されたときにも既存の機能が正常に動作することを確認できる。
  • Apex を本番組織にリリースする場合、または Apex をパッケージとして顧客に配布する場合のコードカバー率要件が満たされる。
  • 本番組織に高品質のアプリケーションを配信することで、本番ユーザーの生産性が向上する。
  • パッケージ登録者に高品質のアプリケーションを配信することで、顧客の信頼が高まる。
メモ

サービスの各メジャーアップグレードの前に、Salesforce では Apex ハンマーというプロセスによってすべての Apex テストを自動的に実行します。ハンマープロセスは現在のバージョンと次のリリースで実行され、テスト結果が比較されます。このプロセスで、サービスアップグレードの結果としてカスタムコードの動作が変更されることは決してありません。ハンマープロセスでは組織を選択して実行するため、すべての組織では実行されません。検出された問題は、特定の条件に基づいて優先順位が付けられます。Salesforce では、各新規リリースの前に、検出されたすべての問題を修正するよう努めています。

お客様のデータのセキュリティを維持することは、Salesforce の最優先課題です。Salesforce では、お客様の組織のいかなるデータも参照したり、変更したりすることはありません。すべてのテストは、安全なデータセンターにおいてコピーを使用して実行されます。

リリースのコードカバー率要件

コードをリリースするまでに、または Lightning Platform AppExchange 用にパッケージ化するまでに、Apex コードの少なくとも 75% のテストが完了し、すべてのテストに合格している必要があります。また、各トリガーについても何らかのテストを行う必要があります。コードカバー率はリリースの要件ですが、この要件を満たすことのみを目的にテストを記述しないでください。必ず、正常・異常のテストケースや、一括・単一レコード処理など、アプリケーションの一般的なユースケースをテストしてください。

テストメソッドの構文

テストメソッドは @isTest アノテーションを使用して定義し、次の構文を使用します。

@isTest static void testName() {

  // code_block

}

@isTest アノテーションでは、括弧で囲まれ空白で区切られた複数の修飾子を使用します。このパラメーターについては、後で説明します。

テストフレームワークでは常にテストメソッドにアクセスできるため、テストメソッドの表示は考慮されず、テストメソッドを公開または非公開のどちらとして宣言しても違いはありません。そのため、構文ではアクセス修飾子が省略されています。

テストメソッドは、テストクラス (@isTest アノテーションが付加されているクラス) で定義されている必要があります。次のサンプルクラスは、テストメソッドが 1 つあるテストクラスの定義を示しています。

@isTest

private class MyTestClass {

  @isTest static void myTest() {

    // code_block

  }

}

テストクラスは、公開にも非公開にもできます。単体テスト専用のテストクラスを使用する場合は、非公開として宣言します。公開テストクラスは通常、テストデータファクトリクラス (後述) に使用されます。

単体テストの例: TemperatureConverter クラスのテスト

次の簡単なコードは、テストメソッドが 3 つあるテストクラスの例です。テストされるクラスメソッドは、入力値に華氏温度を取ります。この温度を摂氏温度に変換して、変換結果を返します。カスタムクラスとそのテストクラスを追加しましょう。

  1. 開発者コンソールで、[File (ファイル)] | [New (新規)] | [Apex Class (Apex クラス)] をクリックし、クラス名に TemperatureConverter と入力して [OK] をクリックします。
  2. デフォルトのクラス本文を次のコードで置き換えます。
    public class TemperatureConverter {
      // Takes a Fahrenheit temperature and returns the Celsius equivalent.
      public static Decimal FahrenheitToCelsius(Decimal fh) {
        Decimal cs = (fh - 32) * 5/9;
        return cs.setScale(2);
      }
    }
  3. Ctrl + S キーを押してクラスを保存します。
  4. 上記の手順を繰り返して、TemperatureConverterTest クラスを作成します。このクラスに次のコードを追加します。
    @isTest
    private class TemperatureConverterTest {
      @isTest static void testWarmTemp() {
        Decimal celsius = TemperatureConverter.FahrenheitToCelsius(70);
        System.assertEquals(21.11,celsius);
      }
      @isTest static void testFreezingPoint() {
        Decimal celsius = TemperatureConverter.FahrenheitToCelsius(32);
        System.assertEquals(0,celsius);
      }
      @isTest static void testBoilingPoint() {
        Decimal celsius = TemperatureConverter.FahrenheitToCelsius(212);
        System.assertEquals(100,celsius,'Boiling point temperature is not expected.');
      }
      @isTest static void testNegativeTemp() {
        Decimal celsius = TemperatureConverter.FahrenheitToCelsius(-10);
        System.assertEquals(-23.33,celsius);
      }
    }

TemperatureConverterTest テストクラスは、別の華氏温度を入力としてメソッドをコールし、メソッドが期待どおりに機能することを検証します。各テストメソッドは、1 種類の入力 (常温、氷点温度、沸点温度、マイナス温度) を検証します。検証は、System.assertEquals() メソッドをコールして行います。このメソッドは 2 つのパラメーター (1 つ目は期待値、2 つ目は実際の値) を取ります。このメソッドの別バージョンでは、3 つ目のパラメーターとして、testBoilingPoint() で使用される、実行される比較を説明する文字列を取ります。この省略可能な文字列は、アサーションが失敗するとログに記録されます。

このクラスのメソッドを実行しましょう。

  1. 開発者コンソールで、[Test (テスト)] | [New Run (新規実行)] をクリックします。
  2. [Test Classes (テストクラス)] の下で、[TemperatureConverterTest] をクリックします。
  3. TemperatureConverterTest クラスのすべてのテストメソッドをテスト実行に追加するには、[Add Selected (選択項目を追加)] をクリックします。
  4. [実行] をクリックします。
  5. [Tests (テスト)] タブに、実行中のテストの状況が表示されます。テスト実行を展開し、実行された個々のテストのリストが表示されるまでさらに展開します。すべてに緑のチェックマークが付いています。 
    開発者コンソールでテスト結果を調べる

テストを実行した後、組織の Apex クラスやトリガーのコードカバー率が自動的に生成されます。コードカバー率は、開発者コンソールの [Tests (テスト)] タブで確認できます。この例では、テストした TemperatureConverter クラスのカバー率が 100% で、次の画像のように表示されます。

開発者コンソールでコードカバー率を表示する

メモ

Apex コードを変更したときは必ずテストを再実行して、コードカバー率の結果を更新します。

開発者コンソールの既知の問題により、テストのサブセットを実行した場合はコードカバー率が正しく更新されません。コードカバー率の結果を更新するには、[Test (テスト)] | [New Run (新規実行)] ではなく、[Test (テスト)] | [Run All (すべて実行)] を使用します。

1 つのテストメソッドで TemperatureConverter クラスのカバー率が十分な結果に終わっても、別の入力をテストしてコードの品質を確保することが重要です。すべてのデータポイントを検証するのが無理であることはわかりきっていますが、一般的なデータポイントや異なる入力範囲をテストすることは可能です。たとえば、正の数と負の数、境界値、無効なパラメーター値を渡し、異常時の動作を検証することができます。TemperatureConverter クラスのテストでは、沸点温度やマイナス温度のような一般的なデータポイントを検証します。

TemperatureConverterTest テストクラスでは、無効な入力と境界条件は対象ではありません。境界条件とは、最小値と最大値です。この例では、温度変換メソッドが Decimal を受け入れるため、Double 値よりも大きな数値を受け入れることができます。無効な入力については、無効な温度はありませんが、唯一の無効な入力は null です。変換メソッドはこの値をどのように処理するでしょうか。この場合、Apex ランタイムがパラメーター変数を参照解決して数式を評価するときに、System.NullPointerException を発生させます。無効な入力をチェックして、該当する場合は null を返すように FahrenheitToCelsius() メソッドを変更し、無効な入力に対する動作を検証するテストを追加できます。

クラスメソッドで使用されている変換数式が正しいため、ここまではすべてのテストに合格しました。でもこれだけではつまらないですよね。エラーをシミュレーションして、アサーションが失敗したときに何が起きるかを確認してみましょう。たとえば、沸点温度テストを変更し、沸点摂氏温度として誤った期待値 (100 ではなく 0) を渡します。これにより、対応するテストメソッドが失敗します。

  1. testBoilingPoint() テストメソッドを次のように変更します。
    @isTest
    static void testBoilingPoint() {
      Decimal celsius = TemperatureConverter.FahrenheitToCelsius(212);
      // Simulate failure
      System.assertEquals(0,celsius,'Boiling point temperature is not expected.');
    }
  2. 同じテストを実行するには、[Tests (テスト)] タブで最新の実行をクリックし、[Test (テスト)] | [Rerun (再実行)] をクリックします。testBoilingPoint() のアサーションが失敗し、致命的なエラー (キャッチできない AssertException) を発生させます。
  3. [Tests (テスト)] タブで最新のテスト実行を展開して結果を確認します。テスト実行で 4 つのテストのうち 1 つが失敗したとレポートされます。失敗の詳細を表示するには、テスト実行をダブルクリックします。次の画像のように、詳細な結果が別のタブに表示されます。
    [代替テキスト: 開発者コンソールで失敗したテストの結果を調べる]
  4. テスト失敗のエラーメッセージを表示するには、失敗したテストの [Errors (エラー)] 列の内部をダブルクリックします。次のように表示されます。Assertion Failed: の横にある説明テキストは、System.assertEquals() ステートメントで指定したテキストです。

    System.AssertException: Assertion Failed: Boiling point temperature is not expected.: Expected: 0, Actual: 100.00

これらのテストメソッドのテストデータは数値であり、Salesforce レコードではありません。Salesforce レコードのテスト方法と、データの設定方法についての詳細は、次の単元で学習します。

コードカバー率を高める

テストを記述するときは、できるだけ高いコードカバー率を達成できるようにします。Lightning プラットフォームでリリースとパッケージに求められる最低限のカバー率である 75% で良しとしないことです。テストでカバーするテストケースが多いほど、コードが堅牢になる可能性が高まります。場合によっては、すべてのクラスメソッドのテストメソッドを記述した後でも、コードカバー率が 100% にならないことがあります。一般的な原因の 1 つは、条件付きコード実行のすべてのデータ値がカバーされていないことです。たとえば、クラスメソッドに if ステートメントがあり、評価条件を満たすかどうかに基づいて異なる分岐が実行される場合、一部のデータ値が無視される傾向にあります。テストメソッドでは、こうした異なる値を考慮する必要があります。

次の例には、2 つの if ステートメントを含む getTaskPriority() クラスメソッドがあります。このメソッドの主なタスクは、与えられたリードの州に基づいて優先度の文字列値を返すことです。メソッドは、最初に州を検証し、州が無効な場合は null を返します。州が CA の場合、メソッドは「High」 (高) を返し、それ以外の州の値には「Normal」 (通常) を返します。

public class TaskUtil {

  public static String getTaskPriority(String leadState) {

    // Validate input

    if(String.isBlank(leadState) || leadState.length() > 2) {

      return null;

    }

    String taskPriority;

    if(leadState == 'CA') {

      taskPriority = 'High'; 

    } else {

      taskPriority = 'Normal';

    }

    return taskPriority;

  }

}
メモ

等価演算子 (==) は、大文字と小文字を区別しない文字列比較を実行します。そのため、文字列を最初に小文字に変換する必要がありません。つまり、'ca' または 'Ca' を渡すと、文字列リテラル 'CA' との等価条件が満たされることになります。

次に、getTaskPriority() メソッドのテストクラスを示します。このテストメソッドでは、1 つの州 ('NY') を指定して getTaskPriority() をコールするだけです。

@isTest

private class TaskUtilTest {

  @isTest
  static void testTaskPriority() {

    String pri = TaskUtil.getTaskPriority('NY');

    System.assertEquals('Normal', pri);

  }

}

開発者コンソールでこのテストクラス (TaskUtilTest) を実行し、このテストでカバーされる、対応する TaskUtil クラスのコードカバー率をチェックしましょう。テスト実行が終了した後、TaskUtil のコードカバー率が 75% と表示されます。開発者コンソールでこのクラスを開くと、次の画像のように、6 つの青い行 (カバーされた行) と 2 つの赤い行 (カバーされていない行) が表示されます。

開発者コンソールに表示される TaskUtil クラスのカバーされた行

行 5 がカバーされないのは、無効な州パラメーターを渡すテストがテストクラスに含まれていなかったためです。同様に、行 11 は、テストメソッドが 'CA' を州として渡さなかったためカバーされませんでした。さらに 2 つのテストメソッドを追加して、これらのシナリオをカバーしましょう。testTaskHighPriority()testTaskPriorityInvalid() テストメソッドを追加した後の完全なテストクラスを次に示します。[Run All (すべて実行)] または [New Run (新規実行)] を使用して、このテストクラスを返した場合、TaskUtil のコードカバー率は 100% になります。

@isTest

private class TaskUtilTest {

  @isTest

  static void testTaskPriority() {

    String pri = TaskUtil.getTaskPriority('NY');

    System.assertEquals('Normal', pri);

  }

  @isTest

  static void testTaskHighPriority() {

    String pri = TaskUtil.getTaskPriority('CA');

    System.assertEquals('High', pri);

  }

  @isTest

  static void testTaskPriorityInvalid() {

    String pri = TaskUtil.getTaskPriority('Montana');

    System.assertEquals(null, pri);

  }

}

テストスイートを作成して実行する

テストスイートとは、まとめて実行する Apex テストクラスのコレクションです。たとえば、リリースの準備をするたび、または Salesforce が新しいバージョンをリリースするたびに実行するテストスイートを作成します。開発者コンソールでテストスイートを設定し、定期的にまとめて実行するテストクラスセットを定義します。

組織のテストクラスが 2 つになりました。この 2 つのクラスは関連付けられていませんが、ここでは関連付けられているものとします。この 2 つのテストクラスを実行したいが、組織でテストを全部実行したくはないという場合があるかも知れません。そのような場合には、両方のクラスを含むテストスイートを作成して、スイートでテストを実行します。

  1. 開発者コンソールで、[Test (テスト)] | [New Suite (新規スイート)] を選択します。
  2. スイート名に TempConverterTaskUtilSuite と入力して、[OK] をクリックします。
  3. [TaskUtilTest] を選択し、CTRL キーを押したまま [TemperatureConverterTest] を選択します。
  4. 選択したテストクラスをスイートに追加するには、> をクリックします。選択した 2 つのテストクラスが表示されたテストスイート編集ウィンドウ
  5. [Save (保存)] をクリックします。
  6. [Test (テスト)] | [New Suite Run (新規スイート実行)] を選択します。
  7. [TempConverterTaskUtilSuite] を選択し、> をクリックして TempConverterTaskUtilSuite を [Selected Test Suites (選択されたテストスイート)] 列に移動します。
  8. [Run Suites (スイートを実行)] をクリックします。
  9. [Tests (テスト)] タブで、実行中のテストの状況を監視します。テスト実行を展開し、実行された個々のテストのリストが表示されるまでさらに展開します。個別のテストメソッドの実行時と同様に、メソッド名をダブルクリックすれば、テストの詳しい結果を確認できます。

テストデータを作成する

テストメソッドで作成された Salesforce レコードは、データベースにコミットされません。テストの実行が終了すると、レコードはロールバックされます。テスト実行後にテストデータをクリーンアップせずに済むため、このロールバック動作はテストには便利です。

デフォルトで Apex テストには、User または Profile オブジェクトなどの設定やメタデータオブジェクトへのアクセス権を除き、組織に既存のデータへのアクセス権がありません。ですので、テスト用のテストデータを設定します。テストデータを作成すると、テストがより堅牢になり、組織のデータの欠落や変更によって発生するエラーを防止できます。テストデータは、テストメソッドで直接作成するか、後で説明するユーティリティテストクラスを使用して作成することができます。

メモ

必ずしもベストプラクティスとは言えませんが、テストメソッドから既存のデータへのアクセスが必要になることがあります。組織のデータにアクセスするには、テストメソッドに @isTest(SeeAllData=true) アノテーションを付加します。この単元のテストメソッド例は、組織データへのアクセス権がないため、SeeAllData パラメーターは使用しません。

もうひとこと...

  • Visual Studio Code 向け Salesforce Apex 拡張機能を使用して Apex テストを実行し、コードの機能性を検証できます。
  • 組織ごとに最大 6 MB の Apex コードを保存できます。@isTest アノテーションが付加されたテストクラスは、この制限に含まれません。
  • テストデータはロールバックされますが、テストに別個のデータベースが使用されるわけではありません。そのため、一意制約がある項目を持つ一部の sObject では、重複する sObject レコードを挿入するとエラーになります。
  • テストメソッドではメールは送信されません。
  • テストメソッドでは、外部サービスへのコールアウトを実行できません。テストでは、疑似コールアウトを使用できます。
  • テストで実行された SOSL 検索は、空の結果を返します。結果を予測可能にするために、Test.setFixedSearchResults() を使用して検索によってレコードが返されるように定義します。

リソース

ハンズオン Challenge の準備をする

この単元のハンズオン Challenge を完了するには、以下からコピーしたコードを使用して VerifyDate という名前の新しい Apex クラスを作成する必要があります。

public class VerifyDate {

  //method to handle potential checks against two dates

  public static Date CheckDates(Date date1, Date date2) {

    //if date2 is within the next 30 days of date1, use date2.  Otherwise use the end of the month

    if(DateWithin30Days(date1,date2)) {

      return date2;

    } else {

      return SetEndOfMonthDate(date1);

    }

  }

  //method to check if date2 is within the next 30 days of date1

  private static Boolean DateWithin30Days(Date date1, Date date2) {

    //check for date2 being in the past

    if( date2 < date1) { return false; }

    //check that date2 is within (>=) 30 days of date1

    Date date30Days = date1.addDays(30); //create a date 30 days away from date1

    if( date2 >= date30Days ) { return false; }

    else { return true; }

  }

  //method to return the end of the month of a given date

  private static Date SetEndOfMonthDate(Date date1) {

    Integer totalDays = Date.daysInMonth(date1.year(), date1.month());

    Date lastDay = Date.newInstance(date1.year(), date1.month(), totalDays);

    return lastDay;

  }

}
無料で学習を続けましょう!
続けるにはアカウントにサインアップしてください。
サインアップすると次のような機能が利用できるようになります。
  • 各自のキャリア目標に合わせてパーソナライズされたおすすめが表示される
  • ハンズオン Challenge やテストでスキルを練習できる
  • 進捗状況を追跡して上司と共有できる
  • メンターやキャリアチャンスと繋がることができる