JP:An Introduction to Apex Code Test Methods
Apexコードテストメソッドの概要
摘要
堅牢でエラーのない Apex コードの開発を促進するためには、単体テストの作成、実行が不可欠です。単体テストはテストメソッドとテストクラスから構成され、特定部分のコードが正しく動作しているかどうかを検証します。
このドキュメントでは、テストメソッドについて説明します。Force.com アプリケーション開発におけるテストメソッドの重要性、テストメソッドの構文、ベストプラクティスについて説明するほか、Visualforce コントローラ、Web サービスを呼び出す Apex コード用のテストメソッドなど、高度な内容にも触れます。
テストメソッドの概要
堅牢でエラーのない Apex コードの開発を促進するためには、テストメソッドの作成、実行が不可欠です。テストメソッドは、特定部分のコードが正しく動作しているかどうかを検証するクラスメソッドです。テストメソッドは Apex コードで記述し、testMethod キーワードでアノテーションします (実際の構文については次のセクションで説明します)。
Apex を運用環境に展開するには、テストメソッドが不可欠です。テストメソッドは、コードをパッケージ化して Force.com AppExchange で公開する際にも必要になります。テストメソッドでは、75% 以上のコードカバレッジを達成することが求められます。テストメソッドは「Apex コードをテストする Apex コード」であると言えます。コードカバレッジは、テストメソッドの実行時に実行した一意の Apex コードの行数を、すべてのトリガとクラスの Apex コード行数の合計で割った値となります。
「テストメソッドは単に Force.com プラットフォームの 1 つの要件にすぎない」とは考えないようにしてください。テストメソッドは補足的な位置付けでとらえるべきものではありません。テストメソッドの作成は Force.com での開発において非常に重要な要素であり、成功の鍵となります。テストメソッドによりテストを自動化して QA 作業を効率化できるほか、バグ修正の確認や将来的な機能拡張の判断に役立つ自動リグレッションテストのフレームワークを実現できます。
この後のセクションでは、テストメソッドの構文、実行方法、主要なベストプラクティスなどを解説します。
テストメソッドの構文と静的メソッド
前述のとおり、テストメソッドは Apex コードで記述します。Apex についての知識があまりないという方は、 Apex コードの概要 を参照してください。
testMethod キーワードを使用したテストメソッドの定義
Apex メソッドを「テストメソッド」として定義するには、メソッドを static として定義し、testMethod キーワードを追加します。テストメソッドはどの Apex クラスでも定義できますが、Apex トリガでは定義できません。次に、簡単な例を示します。
public class myClass {
static testMethod void myTest() {
// Add test method logic using System.assert(), System.assertEquals()
// and System.assertNotEquals() here.
}
}
isTest アノテーションを使用したテストメソッドのクラスの定義
アプリケーションテスト用のコードのみを含んだクラスを定義するには、isTest クラスアノテーションを使用します。テストメソッドがクラス内に存在し、そのクラスにテストメソッド以外のメソッドが含まれていない場合は、isTest アノテーションの使用が最適です。
isTest アノテーションを使用して定義したクラスは、組織当たりの Apex コードの上限 (1 MB) には加算されません。isTest アノテーションを使用して定義したクラスは、必ず private として宣言してください。インターフェースや列挙子として使用することはできません。
次に、構文の例を示します。
@isTest
private class MyTest {
// Methods for testing
}
Test.startTest、Test.stopTest
Apex の静的なシステムメソッドである Test.startTest と Test.stopTest は、ガバナ制限のテスト、すなわち、大量のデータセットを扱うテストシナリオで使用します。
Test.startTest メソッドは、テストが開始されるテストコードの始点を示します。このメソッドは、各テストメソッドで一度だけ呼び出せます。このメソッドを呼び出す前のコード部分では、変数の初期化、データ構造体へのデータ格納など、テストの実行に必要な設定を行います。このメソッドの呼び出し後は、最初の DML ステートメント (INSERT、DELETE など) または最初の Web サービス呼び出しのいずれかによってガバナ制限が適用されます。
Test.stopTest メソッドは、テストが終了するテストコードの終点を示します。このメソッドは、Test.startTest メソッドとペアで使用し、各テストメソッドで一度だけ呼び出せます。このメソッドを呼び出した後のアサーションは、いずれも元のコンテキストで処理されます。
この 2 つの静的メソッドにより、テストメソッドにおいて、データセットの準備や初期化で使用する Apex リソース、ガバナ制限と、実際のテスト実行時に使用する Apex リソース、ガバナ制限とを分離することが可能になります。
次に、この 2 つのメソッドを使用した例を示します。
static testMethod void verifyAccountDescriptionsWhereOverwritten(){
// Perform our data preparation.
List<Account> accounts = new List<Account>{};
for(Integer i = 0; i < 200; i++){
Account a = new Account(Name = 'Test Account ' + i);
accounts.add(a);
}
// Start the test, this changes governor limit context to
// that of trigger rather than test.
test.startTest();
// Insert the Account records that cause the trigger to execute.
insert accounts;
// Stop the test, this changes limit context back to test from trigger.
test.stopTest();
// Query the database for the newly inserted records.
List<Account> insertedAccounts = [SELECT Name, Description
FROM Account
WHERE Id IN :accounts];
// Assert that the Description fields contains the proper value now.
for(Account a : insertedAccounts){
System.assertEquals(
'This Account is probably left over from testing. It should probably be deleted.',
a.Description);
}
}
trigger OverwriteTestAccountDescriptions on Account (before insert) {
for(Account a: Trigger.new){
if (a.Name.toLowerCase().contains('test')){
a.Description =
'This Account is probably left over from testing. It should probably be deleted.';
}
}
}
上記の例は、テストデータを準備するプロセスと実際のテストシナリオを分離する方法をわかりやすく示しています。最初のステップでは、テストシナリオに必要な 200 件の Account を作成しています。この Account レコードを挿入する処理で、ガバナ制限が適用されます。続いて Test.startTest() メソッドと Test.stopTest() メソッドを使用して、テストデータを準備するプロセスで使用する Apex リソースとガバナ制限を、実際のテストシナリオから分離しています。両メソッドの間にある Apex コードに対しては、異なる Apex ガバナ制限とリソースが適用されます。もしテストメソッドでこの 2 つのメソッドを使用しない場合、利用可能な Apex リソースの一部 (DML ステートメントで処理するレコードの総数など) がテストデータ準備の段階で消費されてしまい、それによりテストシナリオがガバナ制限に達してしまう可能性があります。
System.runAs()
通常、すべての Apex コードはシステムモードで実行され、現在のユーザのアクセス権限やレコード共有設定が考慮されることはありません。システムメソッド System.runAs() を使用すると、既存ユーザや新規ユーザのユーザコンテキストを適用したテストメソッドを作成できます。ここでは、ユーザのすべてのレコード共有設定が反映されます。runAs はテストメソッドでのみ使用可能です。runAs を使用したテストメソッドがすべて完了すると、元のシステムコンテキストが適用されます。
なお、runAs() では、データ共有とデータアクセスをテストし、正しく動作することを確認できますが、CRUD (作成、参照、編集、削除の各権限) や項目レベルセキュリティを検証することはできません。
public class TestRunAs {
public static testMethod void testRunAs() {
// Setup test data
// This code runs as the system user
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) {
// The following code runs as user 'u'
System.debug('Current User: ' + UserInfo.getUserName());
System.debug('Current Profile: ' + UserInfo.getProfileId()); }
// Run some code that checks record sharing
}
}
上記のテストメソッドでは、標準ユーザのプロファイルを使用して新規ユーザを作成し、このユーザのコンテキストでコード内のブロックを実行しています。
テストメソッドの実行
テストメソッドを実行する主な方法には、次の 2 つがあります。
- Salesforce CRM ユーザインターフェース
- Force.com IDE
次のセクションでは、各環境でテストメソッドを実行する方法を説明します。
Salesforce CRM ユーザインターフェースを使用したテストメソッドの実行
Salesforce CRM ユーザインターフェースでは、特定のクラスに対するテストメソッドを実行したり、組織で使用しているすべてのテストメソッドを実行したりできます。
特定のクラスに対するテストメソッドを実行するには、[設定]、[開発]、[Apex クラス] の順にメニューを選択し、クラス名をクリックして [テストを実行] をクリックします。別のクラスの呼び出しやトリガの実行が行われる場合、それらの Apex コードの行数もコードカバレッジの対象に加算されます。
組織で使用しているすべてのテストメソッドを実行するには、[設定]、[開発]、[Apex クラス] の順にメニューを選択して [すべてのテストを実行] をクリックします。
テスト結果のページでは、次のような情報を確認できます。
- 実行したテストの数、失敗したテストの数、Apex コードのカバー率など ([概要] セクションに表示)
- 失敗したテスト (失敗した場合のみ)
- テストを行った Apex コードに関する情報 (テストされたコードの行数と列数、コードの実行回数、コードのテストに要した時間など)
- テストを行った Apex コードに関する情報 (コードの行数と列数など)
- テストカバレッジに関する警告 (該当する場合のみ)
- デバッグログ
テスト結果のページのスクリーンショットを次に示します。
Force.com IDE を使用したテストメソッドの実行
テストメソッドは、Salesforce CRM ユーザインターフェースのほか、Force.com IDE でも実行可能です。Force.com IDE についての知識があまりないという方は、Force.com IDE でインストール方法や関連ドキュメントを参照してください。
Force.com IDE では、組織で使用しているすべてのテストメソッドを実行したり、特定の Apex クラス向けのテストメソッドを実行したりできます。
組織で使用しているすべてのテストメソッドを実行するには、Force.com IDE でプロジェクトを選択して展開し、[classes] フォルダを右クリックします。[Force.com] プロパティを選択し、[Run All Test] をクリックします。
結果が出力され、[Apex Test Runner] ビューに表示されます。なお、[Apex Test Runner] ビューを表示するには、Force.com のパースペクティブが選択されている必要があります。このビューでは、実行したテストの数、失敗したテストの数、コードのカバー率、デバッグログなどを確認できます。
テストメソッドのベストプラクティスとヒント
このセクションでは、テストメソッドの作成に関する主なヒントとベストプラクティスをご紹介します。詳細な情報は、How to Write Good Unit Tests (理想的な単体テストの作成方法) を参照してください。単体テストの正しい構造、単体テストでカバーする必要のあるコードシナリオ、理想的な単体テストのプロパティについて、詳しく説明しています。
- テストメソッドには引数がなく、データベースにデータをコミットすることもなく、電子メールの送信は実行できない
- 目標は 100% のコードカバレッジ。最低条件の 75% に甘んじない
- ポジティブケース (コードが仕様どおりに動作することを検証する) とネガティブケース (エラーが適切に処理されることを検証する) の双方を想定し、あらゆるシナリオに対応するテストメソッドを作成します。
- 移植可能なテストメソッドを作成する
- Apex コードは組織内で開発後、他組織の運用環境に展開したり、AppExchange パッケージとしてインストールされることになります。そのため、ID や特定のデータセットを必要とするテストメソッドは作成しないようにします。このようなテストメソッドは他の組織では使用できません。
次に、不適切なテストメソッドの例を示します。このテストメソッドでは、必要なテストデータがハードコーディングされてしまっています。
static testMethod void myTestHardcodedIds(){
// INCORRECT - By hardcoding this Account Id in the test method, the test method
// will fail in every other org where this code is deployed because the hardcoded
// Account Id won't exist there.
Account testAccount = [select id,name from Account where Id='001300000040lMM'];
testAccount.billingState='CA';
update testAccount;
// Verify that the billingState field was updated in the database.
Account updatedAccount = [SELECT billingState FROM Account WHERE Id = :testAccount.Id];
System.assertEquals('CA', updatedAccount.billingState);
// INCORRECT - By hardcoding this Product Name in the test method, the test method
// will fail in every other org where this code is deployed becuase the hardcoded
// Product Id won't exist there.
Product2 prod = [select id, name from Product2 where Name='Router'];
prod.productcode='RTR2000';
update prod;
// Verify that the productcode field was updated in the database.
Product2 updatedProduct = [SELECT productcode FROM Product2 WHERE Id = :prod.Id];
System.assertEquals('RTR2000', updatedProduct.productcode);
}
次に、適切に記述された移植可能なテストメソッドの例を示します。
static testMethod void myTestDynamicIds(){
// CORRECT - Create the required test data needed for the test scenario.
// In this case, I need to update an Account to have a BillingState='CA'
// So I create that Account in my test method itself.
Account testAccount = new Account(name='Test Company Name');
insert testAccount;
testAccount.billingState='CA';
update testAccount;
// Verify that the billingState field was updated in the database.
Account updatedAccount = [SELECT billingState FROM Account WHERE Id = :testAccount.Id];
System.assertEquals('CA', updatedAccount.billingState);
// CORRECT - In this case, I need to update a Product to have a productcode ='RTR2000'
// So I create that Product2 record in my test method itself.
Product2 prod = new Product2(name='Router');
insert prod;
prod.productcode='RTR2000';
update prod;
// Verify that the productcode field was updated in the database.
Product2 updatedProduct = [SELECT productcode FROM Product2 WHERE Id = :prod.Id];
System.assertEquals('RTR2000', updatedProduct.productcode);
}
System.assertメソッドを使用してコードが正常に機能することを検証する
- このメソッドでは、Apex コードが返す値や振る舞いが期待どおりのものであることを確認できるほか、Apex コードの変更に伴うリグレッションテストを実施できます。
- 条件ロジック (三項演算子など) では、すべての条件分岐が実行されるようにする
runAsメソッドを使用して複数のユーザコンテキストでアプリケーションをテストする
- 異なるアクセス権やデータ参照権限を持つさまざまなプロファイルを使用してテストを実行し、Apex コードがあらゆるシナリオで正常に動作することを検証します。
- トリガの一括実行のテストでは最低でも 20 件のレコードを用意する
- すべての Apex コードは一括実行が可能であるため、トリガやクラスを作成する際は、ガバナ制限に抵触せずに複数のレコードを処理できるように注意します。テストでは、「大量のデータセットを扱う Apex コード」というシナリオを必ず用意します。
この後のセクションでは、こうしたベストプラクティスを実現するためのポイントを解説します。
一括処理のテスト
Apex コードは一括処理を行うことから、テストでは、単一レコードのみでなく大量のデータセットの処理が可能な設計がなされており、実際に処理可能であるかどうかを検証するシナリオを用意する必要があります。Apex トリガは、Salesforce CRM インターフェースか Force.com Web サービス API を介したデータ操作によって起動されるようになっていますが、API では一度に複数のレコードを送信できるため、トリガも複数のレコードによって起動されることになります。したがって、テストメソッドでは、すべての Apex コードについて、大量のデータセットの処理が可能な設計がなされており、ガバナ制限に抵触していないことを検証できるという点が重要となります。
では、具体的なコードサンプルを使って、トリガで一括処理を正しく行えずガバナ制限に抵触してしまうテストメソッドを修正する手順を説明します。
まず、不適切な設計の Contact (取引先責任者) トリガの例を次に示します。このトリガは、各 Contact レコードに対して SOQL クエリを実行して、関連する Account (取引先) 情報を取得します。このトリガでは、SOQL クエリが for ループ内に配置されているため、挿入または変更された Contact レコードが 20 件以上になるとガバナ制限に抵触してしまいます。
trigger contactTest on Contact (before insert, before update) {
for(Contact ct: Trigger.new){
Account acct = [select id, name from Account where Id=:ct.AccountId];
if(acct.BillingState=='CA'){
System.debug('found a contact related to an account in california...');
ct.email = 'test_email@testing.com';
//Apply more logic here....
}
}
}
次に示すのは、トリガで大量のデータセットを正しく処理できるかどうかを検証するテストメソッドです。
public class sampleTestMethodCls {
static testMethod void testAccountTrigger(){
//First, prepare 200 contacts for the test data
Account acct = new Account(name='test account');
insert acct;
Contact[] contactsToCreate = new Contact[]{};
for(Integer x=0; x<200;x++){
Contact ct = new Contact(AccountId=acct.Id,lastname='testing',firstname='apex');
contactsToCreate.add(ct);
}
//Now insert data causing an contact trigger to fire.
Test.startTest();
insert contactsToCreate;
Test.stopTest();
}
}
このテストメソッドでは 200 件の Contact レコードを格納した配列を作成して挿入を実行します。この挿入処理により、トリガが起動されます。このテストメソッドでは、ガバナ制限に抵触した場合に System.Exception が返されます。前に示したトリガの場合は Contact ごとに SOQL クエリを実行するため、このテストメソッドにより「Too many SOQL queries: 21」という例外が返され、トリガが実行できるクエリの最大数 (20 件) を超えていることが通知されます。
では、一括処理を正しく行えるようにトリガを修正しましょう。修正のポイントは、SOQL クエリを for ループの外に移動して、一度のみ実行されるように変更するという点です。
trigger contactTest on Contact (before insert, before update) {
Set<Id> accountIds = new Set<Id>();
for(Contact ct: Trigger.new)
accountIds.add(ct.AccountId);
//Do SOQL Query
Map<Id, Account> accounts = new Map<Id, Account>(
[select id, name, billingState from Account where id in :accountIds]);
for(Contact ct: Trigger.new){
if(accounts.get(ct.AccountId).BillingState=='CA'){
System.debug('found a contact related to an account in california...');
ct.email = 'test_email@testing.com';
//Apply more logic here....
}
}
}
上記では Account を取得する SOQL クエリは一度のみ実行されます。先ほどのテストメソッドを再度実行すると、正常に完了し、コードカバレッジは 100% となります。
Visualforce コントローラのテスト
カスタムのコントローラ、およびコントローラ拡張機能についても、Apex コード同様にテストメソッドが必要です。Visualforce コントローラを作成する場合は、同時に適切なテストメソッドも作成するようにします。コントローラで使用するテストメソッドでは、クエリパラメータを指定したり、別のページに移動させるなどの設定を行って、ユーザとの対話を自動化できます。 Apex には、Visualforce コントローラ用テストメソッドの作成を支援する Apex クラスが複数用意されています。
次に、Visualforce Reference Guide (Visualforce リファレンスガイド) で紹介されている例を示します。なお、主な機能については説明のコメントを追加しています。
public static testMethod void testMyController() {
//Use the PageReference Apex class to instantiate a page
PageReference pageRef = Page.success;
//In this case, the Visualforce page named 'success' is the starting point of this test method.
Test.setCurrentPage(pageRef);
//Instantiate and construct the controller class.
thecontroller controller = new thecontroller();
//Example of calling an Action method. Same as calling any other Apex method.
//Normally this is executed by a user clicking a button or a link from the Visualforce
//page, but in the test method, just test the action method the same as any
//other method by calling it directly.
//The .getURL will return the page url the Save() method returns.
String nextPage = controller.save().getUrl();
//Check that the save() method returns the proper URL.
System.assertEquals('/apex/failure?error=noParam', nextPage);
//Add parameters to page URL
ApexPages.currentPage().getParameters().put('qp', 'yyyy');
//Instantiate a new controller with all parameters in the page
controller = new thecontroller();
//Example of calling the 'setter' method for several properties.
//Normally these setter methods are initiated by a user interacting with the Visualforce page,
//but in a test method, just call the setter method directly.
controller.setLastName('lastname');
controller.setFirstName('firstname');
controller.setCompany('acme');
controller.setEmail('firstlast@acme.com');
nextPage = controller.save().getUrl();
//Verify that the success page displays
System.assertEquals('/apex/success', nextPage);
}
ページの参照方法、コントローラのインスタンス化方法、パラメータの追加方法、アクションメソッドの起動方法などを確認してください。
Apex による Web サービス呼び出しのテスト
Apex コードには、Amazon Web サービス、Facebook、Google といった外部の公開 Web サービスを呼び出す機能が組み込まれているため、このような呼び出し処理を行う Apex コードを対象とする適切なテストメソッドが必要となります。ただし、Force.com プラットフォームでは、外部 Web サービスを管理したり呼び出しによる影響を制御したりできないため、テストメソッドでサードパーティの Web サービスを起動することはできません。このセクションでは、そうした状況でコードカバレッジを達成するための方法を説明します。
必要な処理は、テストメソッド側ではなく、主に Web サービスを呼び出す Apex コードの側で行います。具体的には、Apex コードを、次に示す各メソッドにリファクタリングすることをお勧めします。
- Web サービス要求と関連データを作成する Apex メソッド - このメソッドでは Web サービスを起動しません。次に擬似コードを示します。
public HttpRequest buildWebServiceRequest(){
//Build HTTP Request object
HttpRequest req = new HttpRequest();
req.setEndpoint(<insert endpoint url here>);
req.setMethod('GET');
}
- Web サービスを起動する Apex メソッド - このメソッドでは HTTP 要求パラメータを受信して Web サービスを起動し、HTTP 応答オブジェクトを返します。次のように、わずか数行の Apex コードです。
public HttpResponse invokeWebService(Http h, HttpRequest req){
//Invoke Web Service
HttpResponse res = h.send(req);
return res;
}
- HTTP 応答を処理する Apex メソッド - Web サービスの応答後に実行する Apex メソッドです。次に擬似コードを示します。
public void handleWebServiceResponse(HttpResponse res){
//Parse and apply logic to the res message
}
これらにより Web サービスの実行処理を、要求と応答のサブセットを処理する 3 つのセクションに分割します。この 3 つの Apex メソッドは、次の例のように main () から順番に起動できます。
public void main(){
//apply business logic
//now need to make web service callout
//First, build the http request
Http h = new Http();
HttpRequest req = buildWebServiceRequest();
//Second, invoke web service call
HttpResponse res = invokeWebService(h, req);
//Last, handling the response
handleWebServiceResponse(res);
//continue with the apex script logic
}
以上のようにリファクタリングしたコードを使用することで、テストメソッドで必要なコードカバレッジを達成できます。Web サービス処理を 3 つのメソッドに分割することで、ほとんどの Apex コードがテスト可能になります。なお、次に示すような小さな呼び出しのメソッドは対象外となります。
static testMethod void testWebService(){
//First, build the http request
HttpRequest req = buildWebServiceRequest();
//NOTE - WE DO NOT EXECUTE THE METHOD, invokeWebService.
//Now, since we can't execute the actual web service,
//write apex code to build a sample HttpResponse object
HttpResponse res = new HttpResponse();
//Apply test data and attributes to the HttpResponse object as needed
handleWebServiceResponse(res);
}
ここで要点をまとめると、Apex コードを Web サービス要求の作成、Web サービスの起動、Web サービス応答の処理という 3 つのメソッドにリファクタリングすることで、テストメソッドにおいて、Web サービスを実際に起動することなく要求、応答の処理を検証できるようになります。
まとめ
このドキュメントではテストメソッド作成の概要を説明し、テストメソッドの構文、テストの実行方法、ベストプラクティスなどを紹介しました。テストは開発の進展を妨げる足かせではなく、成功の鍵です。テストメソッドの検証や作成を二次的なものとして軽視しないでください。テストメソッドの作成は、開発作業と並行して取り組む必要があります。テストメソッドにより、テスト自動化による QA 作業の効率化や、リグレッションテストの実施などが可能になります。
参考資料
- Apex Wiki ページ: Apex コードに関するリソースの包括的なリストを参照できます。
- How to Write Good Unit Tests (理想的な単体テストの作成方法): 理想的な単体テストを作成するために役立つ追加情報が記載されています。
- Force.com IDE: Force.com IDE について学べるテクニカルライブラリです。
- Testing Best Practices (テストのベストプラクティス): Force.com Apex コード開発者向けガイド内のトピック。テストのベストプラクティスについて解説しています。
- Testing Example (テストのサンプル): Force.com Apex コード開発者向けガイド内のトピック。テストのサンプルを参照できます。
- Governors in Apex Code (Apex コードのガバナ): Apex コードのガバナについて包括的に学べるテクニカルライブラリです。
- Apex Test Coverage Best Practices (Apex テストカバレッジのベストプラクティス): Dreamforce 2008 のセッションを収録したビデオです。テストの作成に役立つアドバイスが紹介されています。
執筆者について
Andrew Albert は、Force.com プラットフォームを中心にセールスフォース・ドットコムテクノロジの普及、啓蒙を担当するテクニカルエバンジェリストです。




