Apex 単体テストを始める
学習の目的
この単元を完了すると、次のことができるようになります。
- Apex 単体テストの主要な利点を説明する。
- クラスとそのテストメソッドを定義する。
- クラスのすべてのテストメソッドを実行し、エラーを調べる。
- 一連のテストクラスを作成し、実行する。
Apex 単体テスト
Apex テストフレームワークでは、Apex クラスとトリガーのテストを記述して実行できます。Apex 単体テストにより、Apex コードの品質が高まり、Apex のリリース要件を満たすことができます。
テストは、長期的な開発を成功に導く鍵であり、開発プロセスにおいて不可欠な要素です。Apex テストフレームワークを使用すれば、Apex コードは簡単にテストできます。Apex コードは、Sandbox または開発者組織でのみ記述でき、本番組織では記述できません。Apex コードは、Sandbox から本番組織にリリースできます。また、アプリケーション開発者は、パッケージを Salesforce AppExchange にアップロードすることで、Apex コードを開発者組織から顧客に配布できます。Apex 単体テストは、品質保証にとって不可欠なだけでなく、Apex のリリースと配布の要件でもあります。
Apex 単体テストによってコードの品質を改善できます。Apex テストフレームワークを使用すると、次のような利点があります。
- Apex クラスやトリガーが期待どおりに機能することを確認できる。
- 一連の回帰テストを用意することで、クラスやトリガーが更新されるたびに再実行でき、将来アプリケーションが更新されたときにも既存の機能が正常に動作することを確認できる。
- Apex を本番組織にリリースする場合、または Apex をパッケージとして顧客に配布する場合のコードカバー率要件が満たされる。
- 本番組織に高品質のアプリケーションを配信することで、本番ユーザーの生産性が向上する。
- パッケージ登録者に高品質のアプリケーションを配信することで、顧客の信頼が高まる。
リリースのコードカバー率要件
コードをリリースするまでに、または Salesforce AppExchange 用にパッケージ化するまでに、Apex コードの少なくとも 75% のテストが完了し、すべてのテストに合格している必要があります。また、各トリガーについても何らかのテストを行う必要があります。コードカバー率はリリースの要件ですが、この要件を満たすことのみを目的にテストを記述しないでください。必ず、正常・異常のテストケースや、一括・単一レコード処理など、アプリケーションの一般的なユースケースをテストしてください。
テストクラスとテストメソッドの構文
Apex テストクラスには @isTest アノテーションが付加されます。テストクラスは、公開または非公開アクセス修飾子のどちらを使用して定義することもできます。単体テスト専用のテストクラスを使用する場合は、非公開として宣言します。公開テストクラスは通常、テストデータファクトリクラスで使用します。このクラスについては、「Apex テストのテストデータを作成する」単元で説明します。@isTest として定義されるクラスは、最上位クラスである必要があります。
テストクラスでは、テストメソッドも @isTest アノテーションを使用して定義します。テストフレームワークでは常にテストメソッドにアクセスできるため、テストメソッドの表示は考慮されず、テストメソッドを公開または非公開のどちらとして宣言しても違いはありません。そのため、メソッドの構文ではアクセス修飾子が省略されています。
次のサンプルコードは、テストメソッドが 1 つあるテストクラスの定義を示しています。
@isTest
private class MyTestClass {
@isTest static void myTest() {
// code_block
}
}@isTest アノテーションでは、括弧で囲まれ空白で区切られた複数の修飾子を使用できます。@isTest(seeAllData=true) アノテーションについては、「Apex テストのテストデータを作成する」単元で簡単に説明します。
単体テストの例: TemperatureConverter クラスのテスト
次の簡単なコードは、テストメソッドが 3 つあるテストクラスの例です。テストされるクラスメソッドは、入力値に華氏温度を取ります。この温度を摂氏温度に変換して、変換結果を返します。カスタムクラスとそのテストクラスを追加しましょう。
- 開発者コンソールで、[File (ファイル)] | [New (新規)] | [Apex Class (Apex クラス)] をクリックし、クラス名に
TemperatureConverterと入力して [OK] をクリックします。
- デフォルトのクラス本文を次のコードで置き換えます。
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); } } -
Ctrl + S キーを押してクラスを保存します。
- 上記の手順を繰り返して、
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 種類の入力 (常温、氷点温度、沸点温度、マイナス温度) を検証します。検証を実行するには、2 つのパラメーターを指定して System.assertEquals() メソッドをコールします。最初のパラメーターは想定される値で、2 番目のパラメーターは実際の値です。このメソッドでは、比較の内容を説明する文字列を 3 番目のパラメーターとして指定することもできます。この省略可能な 3 番目のパラメーターは、testBoilingPoint() の例で使用されています。指定された文字列は、アサーションが失敗するとログに記録されます。
このクラスのメソッドを実行しましょう。
- 開発者コンソールで、[Test (テスト)] | [New Run (新規実行)] をクリックします。
-
[Test Classes (テストクラス)] の下で、[TemperatureConverterTest] をクリックします。
-
TemperatureConverterTestクラスのすべてのテストメソッドをテスト実行に追加するには、[Add Selected (選択項目を追加)] をクリックします。
-
[実行] をクリックします。
- [Tests (テスト)] タブに、実行中のテストの状況が表示されます。テスト実行を展開し、実行された個々のテストのリストが表示されるまでさらに展開します。すべてに緑のチェックマークが表示されており、すべてのテストに合格したことを示しています。

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

1 つのテストメソッドで TemperatureConverter クラスのカバー率が十分な結果に終わっても、別の入力をテストしてコードの品質を確保することが重要です。すべてのデータポイントを検証するのが無理であることはわかりきっていますが、一般的なデータポイントや異なる入力範囲をテストすることは可能です。たとえば、正の数と負の数、境界値、無効なパラメーター値を渡し、異常時の動作を検証することができます。TemperatureConverter クラスのテストでは、氷点や沸点、そしてプラスやマイナスの温度のような一般的なデータポイントを検証します。
ただし、TemperatureConverterTest テストクラスは、現在、境界条件や無効な入力には対応していません。境界条件とは、メソッドが処理できる最小値および最大値のことです。無効な入力とは、FahrenheitToCelsius() に引数として null が渡された場合などです。この場合、Apex ランタイムがパラメーター変数を参照解決して数式を評価するときに、System.NullPointerException を発生させます。このエラーを処理するには、無効な入力を検出した場合に null を返すように FahrenheitToCelsius() メソッドを修正します。そして、TemperatureConverterTest クラスにテストメソッドを追加して、無効な入力に対する動作を検証します。
クラスメソッドで使用されている変換数式が正しいため、ここまではすべてのテストに合格しました。でもこれだけではつまらないですよね。エラーをシミュレーションして、アサーションが失敗したときに何が起きるかを確認してみましょう。たとえば、沸点温度テストを変更し、沸点摂氏温度として誤った期待値 (100 ではなく 0) を渡します。これにより、対応するテストメソッドが失敗します。
-
testBoilingPoint()テストメソッドを次のように変更します。@isTest static void testBoilingPoint() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(212); // Simulate failure System.assertEquals(0,celsius,'Boiling point temperature is not expected.'); } - 同じテストを実行するには、[Tests (テスト)] タブで最新のテスト実行を選択してから、[Test (テスト)] | [Rerun (再実行)] をクリックします。
testBoilingPoint()のアサーションが失敗し、致命的なエラー (キャッチできないAssertException) が発生します。
- [Tests (テスト)] タブで最新のテスト実行を展開して結果を確認します。テスト実行で 4 つのテストのうち 1 つが失敗したとレポートされます。失敗の詳細を表示するには、テスト実行をダブルクリックします。詳細な結果が別のタブに表示されます。
- テスト失敗のエラーメッセージを表示するには、失敗したテストの [Errors (エラー)] 列の内部をダブルクリックします。エラーメッセージには、
System.assertEquals()ステートメントで指定したテキストが含まれます。System.AssertException: Assertion Failed: Boiling point temperature is not expected.: Expected: 0, Actual: 100.00
これらのテストメソッドのテストデータは数値であり、Salesforce レコードではありません。Salesforce レコードのテスト方法と、データの設定方法についての詳細は、次の単元で学習します。
コードカバー率を高める
テストを記述するときは、できるだけ高いコードカバー率を達成できるようにします。Salesforce でリリースとパッケージに求められる最低限のカバー率である 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;
}
}次に、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% と表示されます。
開発者コンソールの TaskUtil クラスに戻ります。まだ設定を変更していない場合は、[Code Coverage: None (コードカバー率: なし)] から [Code Coverage: All Tests (コードカバー率: すべてのテスト)] に変更します。この設定では、次の画像のように、6 つの青い行 (カバーされた行) と 2 つの赤い行 (カバーされていない行) が表示されます。

行 5 がカバーされないのは、無効な州パラメーターを渡すテストがテストクラスに含まれていないためです。同様に、行 11 は、テストメソッドが「CA」を州として渡さないためカバーされません。さらに 2 つのテストメソッドを追加して、これらのシナリオをカバーしましょう。testTaskHighPriority() と testTaskPriorityInvalid() テストメソッドを追加した後の完全なテストクラスを次に示します。[Test (テスト)] | [Run All (すべて実行)] または [Test (テスト)] | [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 つのテストクラスを実行したいが、組織でテストを全部実行したくはないという場合があるかも知れません。そのような場合には、両方のクラスを含むテストスイートを作成して、スイートでテストを実行します。
- 開発者コンソールで、[Test (テスト)] | [New Suite (新規スイート)] を選択します。
- スイート名に
TempConverterTaskUtilSuiteと入力して、[OK] をクリックします。
-
[TaskUtilTest] を選択し、CTRL キーを押したまま [TemperatureConverterTest] を選択します。
- 選択したテストクラスをスイートに追加するには、> をクリックします。

-
[Save (保存)] をクリックします。
-
[Test (テスト)] | [New Suite Run (新規スイート実行)] を選択します。
-
[TempConverterTaskUtilSuite] を選択し、> をクリックして
TempConverterTaskUtilSuiteを [Selected Test Suites (選択されたテストスイート)] 列に移動します。
-
[Run Suites (スイートを実行)] をクリックします。
- [Tests (テスト)] タブで、実行中のテストの状況を監視します。テスト実行を展開し、実行された個々のテストのリストが表示されるまでさらに展開します。個別のテストメソッドの実行時と同様に、メソッド名をダブルクリックすれば、テストの詳しい結果を確認できます。
テストデータを作成する
テストメソッドで作成された Salesforce レコードは、データベースにコミットされません。テストの実行が終了すると、レコードはロールバックされます。テスト実行後にテストデータをクリーンアップせずに済むため、このロールバック動作はテストには便利です。
デフォルトで Apex テストには、User または Profile オブジェクトなどの設定やメタデータオブジェクトへのアクセス権を除き、組織に既存のデータへのアクセス権がありません。ですので、テスト用のテストデータを設定します。テストデータを作成すると、テストがより堅牢になり、組織のデータの欠落や変更によって発生するエラーを防止できます。テストデータは、テストメソッドで直接作成するか、後で説明するユーティリティテストクラスを使用して作成することができます。
もうひとこと...
- Visual Studio Code 向け Salesforce Apex 拡張機能を使用して Apex テストを実行し、コードの機能性を検証できます。
- 組織ごとに最大 6 MB の Apex コードを保存できます。
@isTestアノテーションが付加されたテストクラスは、この制限に含まれません。
- テストデータはロールバックされますが、テストに別個のデータベースが使用されるわけではありません。そのため、一意制約がある項目を持つ一部の sObject では、重複する sObject レコードを挿入するとエラーになります。
- テストメソッドではメールは送信されません。
- テストメソッドでは、外部サービスへのコールアウトを実行できません。テストでは、疑似コールアウトを使用できます。
- テストで実行された SOSL 検索は、空の結果を返します。結果を予測可能にするために、
Test.setFixedSearchResults()を使用して検索によってレコードが返されるように定義します。
リソース
- Apex 開発者ガイド: ベストプラクティスのテスト
- Apex 開発者ガイド: Apex の単体テスト
- Apex 開発者ガイド: 単体テストの組織データとテストデータの分離
- Salesforce ヘルプ: コードカバー率のチェック
ハンズオン 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;
}
}