Apex 単体テストを始める
学習の目的
この単元を完了すると、次のことができるようになります。
- Apex 単体テストの主要な利点を説明する。
- クラスとそのテストメソッドを定義する。
- クラスのすべてのテストメソッドを実行し、エラーを調べる。
- 一連のテストクラスを作成し、実行する。
Apex 単体テスト
Apex テストフレームワークでは、Apex クラスとトリガーのテストを記述して実行できます。Apex 単体テストにより、Apex コードの品質が高まり、Apex のリリース要件を満たすことができます。
テストは、長期的な開発を成功に導く鍵であり、開発プロセスにおいて不可欠な要素です。Apex テストフレームワークを使用すれば、Apex コードは簡単にテストできます。Apex コードは、Sandbox または開発者組織でのみ記述でき、本番組織では記述できません。Apex コードは、Sandbox から本番組織にリリースできます。また、アプリケーション開発者は、パッケージを Salesforce AgentExchange にアップロードすることで、Apex コードを開発者組織から顧客に配布できます。Apex 単体テストは、品質保証にとって不可欠なだけでなく、Apex のリリースと配布の要件でもあります。
Apex 単体テストによってコードの品質を改善できます。Apex テストフレームワークを使用すると、次のような利点があります。
- Apex クラスやトリガーが期待どおりに機能することを確認できる。
- 一連の回帰テストを用意することで、クラスやトリガーが更新されるたびに再実行でき、将来アプリケーションが更新されたときにも既存の機能が正常に動作することを確認できる。
- Apex を本番組織にリリースする場合、または Apex をパッケージとして顧客に配布する場合のコードカバー率要件が満たされる。
- 本番組織に高品質のアプリケーションを配信することで、本番ユーザーの生産性が向上する。
- パッケージ登録者に高品質のアプリケーションを配信することで、顧客の信頼が高まる。
リリースのコードカバー率要件
コードをリリースするまでに、または Salesforce AgentExchange 用にパッケージ化するまでに、Apex コードの少なくとも 75% のテストが完了し、すべてのテストに合格している必要があります。また、各トリガーについても何らかのテストを行う必要があります。コードカバー率はリリースの要件ですが、この要件を満たすことのみを目的にテストを記述しないでください。必ず、正常・異常のテストケースや、一括・単一レコード処理など、アプリケーションの一般的なユースケースをテストしてください。
テストクラスとテストメソッドの構文
Apex テストクラスには @IsTest アノテーションが付加されます。テストクラスは、公開または非公開アクセス修飾子のどちらを使用して定義することもできます。単体テスト専用のテストクラスを使用する場合は、非公開として宣言します。公開テストクラスは通常、テストデータファクトリクラスで使用します。このクラスについては、「Apex テストのテストデータを作成する」単元で説明します。@IsTest として定義されたクラスは最上位クラスである必要があります。
テストクラス内では、テストメソッドも @IsTest アノテーションを使用して定義されます。テストフレームワークでは常にテストメソッドにアクセスできるため、テストメソッドの可視性は考慮されず、テストメソッドを公開または非公開のどちらで宣言しても違いはありません。そのため、メソッドの構文ではアクセス修飾子が省略されています。
次のサンプルコードは、テストメソッドが 1 つあるテストクラスの定義を示しています。
@IsTest
private with sharing class MyTestClass {
@IsTest static void myTest() {
// code_block
}
}@IsTest アノテーションでは、括弧で囲まれ空白で区切られた複数の修飾子を使用できます。@IsTest(seeAllData=true) アノテーションについては、「Apex テストのテストデータを作成する」単元で簡単に説明します。
単体テストの例: TemperatureConverter クラスのテスト
次の簡単なコードは、テストメソッドが 3 つあるテストクラスの例です。テストされるクラスメソッドは、入力値に華氏温度を取ります。この温度を摂氏温度に変換して、変換結果を返します。カスタムクラスとそのテストクラスを追加しましょう。
- クイックアクセスメニュー (
) から Web コンソールを開きます。
- Web コンソールで、Ctrl+Shift+P (Windows) または ⌘+Shift+P (macOS) を押してコマンドパレットを開きます。
-
SFDX: Create Apex Class(SFDX: Apex クラスを作成) と入力し、Enter キーを押します。
- テンプレートの種別に [DefaultApexClass] を選択し、Enter キーを押します。
- クラス名に
TemperatureConverterと入力し、Enter キーを押します。
- 出力ディレクトリに [force-app/main/default/classes] を選択し、Enter キーを押します。
- デフォルトのクラス本文を次のコードで置き換えます。
public with sharing 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 (Windows) または ⌘+S (macOS) を押します。
- 上記の手順を繰り返して、
TemperatureConverterTestクラスを作成します。デフォルトのクラス本文を次のコードで置き換えます。@IsTest private with sharing class TemperatureConverterTest { @IsTest static void testWarmTemp() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(70); Assert.areEqual(21.11,celsius); } @IsTest static void testFreezingPoint() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(32); Assert.areEqual(0,celsius); } @IsTest static void testBoilingPoint() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(212); Assert.areEqual(100,celsius,'Boiling point temperature is not expected.'); } @IsTest static void testNegativeTemp() { Decimal celsius = TemperatureConverter.FahrenheitToCelsius(-10); Assert.areEqual(-23.33,celsius); } }
TemperatureConverterTest テストクラスは、別の華氏温度を入力としてメソッドをコールし、メソッドが期待どおりに機能することを検証します。各テストメソッドは、1 種類の入力 (常温、氷点温度、沸点温度、マイナス温度) を検証します。検証を実行するには、2 つのパラメーターを指定して Assert.areEqual() メソッドをコールします。最初のパラメーターは想定される値で、2 番目のパラメーターは実際の値です。このメソッドでは、比較の内容を説明する文字列を 3 番目のパラメーターとして指定することもできます。この省略可能な 3 番目のパラメーターは、testBoilingPoint() の例で使用されています。指定された文字列は、アサーションが失敗するとログに記録されます。
コードカバー率の結果を取得できるようにする
テストを実行する前に、Web コンソールでコードカバー率の結果を取得できるようにします。
- Web コンソールで、アクティビティバーの上部にある
をクリックします。
-
[File (ファイル)] | [Preferences (個人設定)] | [Settings (設定)] を選択し、[Settings (設定)] ウィンドウを開きます。
- 検索ボックスに
retrieveと入力します。
-
[Salesforcedx-vscode-apex-testing: Retrieve-test-code-coverage] 設定を有効にします。
- ウィンドウを閉じます。
ここで、このテストクラスのメソッドを実行してみましょう。
- Web コンソールで、アクティビティバーにある
をクリックします。[Testing (テスト)] サイドパネルが開きます。
- [Testing (テスト)] 検索バーに
TemperatureConverterTestと入力します。
- クラスのすべてのテストメソッドを実行するには、TemperatureConverterTest の行にカーソルを置き、
をクリックします。
[Tests Results (テスト結果)] パネルに、テスト結果が表示されます。[Testing (テスト)] サイドパネルでは、テストメソッドに緑色のチェックマークが付けられ、すべてのテストに合格したことが示されます。また、Web コンソールで、テスト結果の JSON ファイルとマークダウンファイルが生成され、.sfdx/tools/testresults/apex ディレクトリに保存されます。
![TemperatureConverterTest クラスのテスト結果。[Testing (テスト)] サイドパネルのアイコン、[Testing (テスト)] サイドパネルの合格したテストメソッド、テスト結果のマークダウンファイル、[Test Results (テスト結果)] パネルが強調表示されています。](https://res.cloudinary.com/hy4kyit2a/f_auto/fl_lossy/q_70/learn/modules/apex_testing/apex_testing_intro/images/ja-JP/3a85897efa9aedd80384e6b391a6a4f6_kix.xab97sh0jqum.png)
テストを実行した後、組織の Apex クラスやトリガーのコードカバー率が自動的に生成されます。テスト結果のマークダウンファイルで、コードカバー率を確認できます。この例では、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
Assert.areEqual(0,celsius,'Boiling point temperature is not expected.');
}- [Testing (テスト)] サイドパネルから、TemperatureConverterTest クラスのすべてのテストメソッドを実行します。
testBoilingPoint()のアサーションが失敗し、致命的なエラー (キャッチできないAssertException) が発生します。
- [Test Results (テスト結果)] パネルで結果を確認します。テスト実行で 4 つのテストのうち 1 つが失敗したとレポートされます。
- テスト失敗のエラーメッセージを取得するには、[Testing (テスト)] サイドパネルから [testBoilingPoint] を選択します。エラーメッセージには、
Assert.areEqual()ステートメントで指定したテキストが含まれます。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 with sharing 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 with sharing class TaskUtilTest {
@IsTest
static void testTaskPriority() {
String pri = TaskUtil.getTaskPriority('NY');
Assert.areEqual('Normal', pri);
}
}では、Web コンソールでこのテストクラス (TaskUtilTest) を実行し、このテストでカバーされる、対応する TaskUtil クラスのコードカバー率を見てみましょう。テスト実行が終了した後、TaskUtil のコードカバー率が 75% と表示されます。
ここで、TaskUtil.cls ファイルを開きます。このファイルを開いた状態で、Web コンソールウィンドウの下部にある
をクリックして、[Highlight Apex Code Coverage (Apex コードカバー率を強調表示)] 設定を有効にします。この設定を有効にすると、次の画像のように、TaskUtil クラスでカバーされた 6 つの行が緑で、カバーされなかった 2 つの行が赤で表示されます。
![Web コンソールの TaskUtil クラスのカバーされた行。[Highlight Apex Code Coverage (Apex コードカバー率の強調表示)] 設定が有効になっています。](https://res.cloudinary.com/hy4kyit2a/f_auto/fl_lossy/q_70/learn/modules/apex_testing/apex_testing_intro/images/ja-JP/f012c10492fbb7b65e58026c9284e06c_kix.nrty1wxlr0sm.png)
行 5 がカバーされないのは、無効な州パラメーターを渡すテストがテストクラスに含まれていないためです。同様に、行 11 は、テストメソッドが「CA」を州として渡さないためカバーされません。さらに 2 つのテストメソッドを追加して、これらのシナリオをカバーしましょう。testTaskHighPriority() と testTaskPriorityInvalid() テストメソッドを追加した後の完全なテストクラスを次に示します。このテストクラスを再実行すると、TaskUtil のコードカバー率が 100% になります!
@IsTest
private with sharing class TaskUtilTest {
@IsTest
static void testTaskPriority() {
String pri = TaskUtil.getTaskPriority('NY');
Assert.areEqual('Normal', pri);
}
@IsTest
static void testTaskHighPriority() {
String pri = TaskUtil.getTaskPriority('CA');
Assert.areEqual('High', pri);
}
@IsTest
static void testTaskPriorityInvalid() {
String pri = TaskUtil.getTaskPriority('Montana');
Assert.isNull(pri);
}
}テストスイートを作成して実行する
テストスイートとは、まとめて実行する Apex テストクラスのコレクションです。たとえば、リリースの準備をするたび、または Salesforce が新しいバージョンをリリースするたびに実行するテストスイートを作成します。開発者コンソールでテストスイートを設定し、定期的にまとめて実行するテストクラスセットを定義します。
組織のテストクラスが 2 つになりました。この 2 つのクラスは関連付けられていませんが、ここでは関連付けられているものとします。この 2 つのテストクラスを実行したいが、組織でテストを全部実行したくはないという場合があるかも知れません。そのような場合には、両方のクラスを含むテストスイートを作成して、スイートでテストを実行します。
- Web コンソールで、Ctrl+Shift+P (Windows) または ⌘+Shift+P (macOS) を押してコマンドパレットを開きます。
-
SFDX: Create Apex Test Suiteと入力し、Enter キーを押します。
- スイート名に
TempConverterTaskUtilSuiteと入力し、Enter キーを押します。
-
[TaskUtilTest] と [TemperatureConverterTest] を選択し、Enter キーを押します。
- アクティビティバーの
をクリックします。[Testing (テスト)] サイドパネルが開きます。
- [Testing (テスト)] 検索バーに
TempConverterTaskUtilSuiteと入力します。
- テストスイートのすべてのテストを実行するには、TempConverterTaskUtilSuite の行にカーソルを置き、
をクリックします。
テストデータを作成する
テストメソッドで作成された 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 with sharing 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;
}
}