はじめに
Salesforce Connectを利用すると、外部のデータをSalesforce内のオブジェクトと同じような感じで、取得できて便利ですよね。
ですがテストクラスでは、内部オブジェクトと同じ感覚で外部オブジェクトのテストを書こうとすると、次のような問題に直面し、テストでハマってしまいました...。
- DML操作で外部オブジェクトのテストレコードを作成できない
-
@IsTest(SeeAllData=true)を試すも、レコードを取得できない - 結果として、SOQLを書いても0件で返る
そもそも外部オブジェクトを利用する人が少ないためか、日本語で書かれた記事などがほとんどなく、私自身、最初は「どうやってテストすればいいの…?」とかなり悩みました。
本記事では、外部オブジェクトをテストクラスで扱うための方法について、記載します。
以下Trailheadで、お手軽に外部オブジェクトを試すことができます。
外部データソースと外部オブジェクトを作成する
対象読者
- Salesforce開発者
- Apexで外部オブジェクトを利用している人
- テストクラスで外部オブジェクトのレコードが取得できず、困っている人
なぜ外部オブジェクトのレコードがテストクラスで取得できないのか
外部オブジェクトの特徴
外部オブジェクトは、Salesforce Connect などを利用して、Salesforce外のデータソースを参照するオブジェクトです。
テスト実行時には、外部データソースへのアクセスが制限されているため、テスト用のレコードを用意する必要があります。
しかし外部オブジェクトは、カスタムオブジェクトのように DML 操作でテストレコードを作成できません。
そのため、結果的に取得レコードが0件となります。
List<ExternalObject__x> records = [SELECT Id, Name FROM ExternalObject__x];
System.debug(records.size()); // 0
外部オブジェクトのテストには2つのアプローチがある
外部オブジェクトをテストする方法は、大きく分けて次の2つがあります。
- アプローチ①:レコードインスタンスを直接生成して渡す
- アプローチ②:SoqlStubProviderを使う
上記2つは テストできる範囲が異なります。
そのため、目的に応じて使い分ける必要があります。
アプローチ①:レコードインスタンスを直接生成して渡す
外部オブジェクトのインスタンスを作成し、メソッドの引数として渡してテストする方法です。
テストできること
- if / for などのロジック
- 計算処理
- 単純なデータ加工など
テストできないこと
- SOQLのテスト
非常にシンプルで、SOQLを含まないテストには有効です。
この方法を利用すれば、外部レコードを利用するメソッドのロジック部分のカバレッジは、ほとんど満たすことができます。
ただし、SOQL自体が正しく書けているかどうかはテストできないため、この方法だけでは不十分になるケースがあります。
アプローチ②:SoqlStubProviderを使う
SOQLの実行結果をテスト用に差し替える方法です。
この方法では、アプローチ①ではテストできなかった、
- SOQLが実行されているか
- WHERE句や条件が正しいか
といった点までテストできます。
一方で SoqlStubProvider を利用した場合、外部レコードは読み取り専用となるため、
- 取得した外部レコードを書き換える
といった処理では、「System.SObjectException: Record is read-only」エラーとなり、テストができません。
SoqlStubProviderとは?
SoqlStubProviderは、テスト実行時にSOQLの実行結果をスタブに差し替える仕組みです。
SoqlStubProvider を使うと、SOQL自体は実行されるが、実際のデータ取得部分だけが差し替えられるという挙動になります。
公式ドキュメント(Mock SOQL Tests for External Objects)にある通り、
- System.SoqlStubProvider を継承したクラスを作成
- handleSoqlQuery を override
- Test.createSoqlStub()を実行
という流れで利用します。
記事公開時点では、Apexリファレンスガイドの言語が日本語だと「SoqlStubProvider」に関する情報が表示されませんでした。
もし表示できなかった場合は「言語を英語に変更」、「ドキュメントのバージョンを変更」することで、表示されるようになるかもしれません。
実装例:SoqlStubProviderを使ったテスト
ApexのAPI Versionは「65.0」です。
テスト対象のコード
public class OrderService {
/**
* OrderIdを条件に外部Orderを取得する
*/
public static Order__x findByOrderId(Integer orderId) {
List<Order__x> orders = [
SELECT
Id
, orderID__c
, orderDate__c
FROM Order__x
WHERE orderID__c = :orderId
];
return orders.isEmpty() ? null : orders[0];
}
}
SoqlStubProviderの実装
@IsTest
public class OrderSoqlStubProvider extends SoqlStubProvider {
public override List<SObject> handleSoqlQuery(SObjectType sObjectType, String rawQuery, Map<String, Object> binds) {
// クエリ対象オブジェクトの検証
System.assertEquals(Order__x.SObjectType, sObjectType, 'Order__x がクエリ対象になっていません');
Integer orderId = 987654321;
// Stubレコードを作成
Order__x order = (Order__x)Test.createStubQueryRow(
sObjectType
, new Map<String, Object>{
'orderID__c' => orderId
, 'customerID__c' => 123456789
}
);
return new List<Order__x>{ order };
}
}
引数の「binds」には、SOQL実行時の各バインド変数のKey/Valueが含まれます。
「binds」を利用することで、SOQLのWHERE句の内容によって返却するStubレコードを切り分けることも可能です。
ただし、実行するSOQLの種類(静的SOQL、Database.query、Database.queryWithBinds)によっては、nullになったり、バインド変数名がKeyに反映されなかったりします。
この辺りは細かいので、別途記事にできたらと思います。
テストクラスでTest.createSoqlStubを実行
スタブ設定した後に、通常通りテストクラスを書けば、外部レコードを取得する処理をテストすることができます。
@IsTest
private class OrderServiceTest {
@IsTest
public static void testFindByOrderId() {
SoqlStubProvider stub = new OrderSoqlStubProvider();
// スタブを設定
Test.createSoqlStub(Order__x.sObjectType, stub);
Test.startTest();
Integer orderId = 987654321;
Order__x order = OrderService.findByOrderId(orderId);
Test.stopTest();
System.assertNotEquals(null, order);
System.assertEquals(orderId, order.orderID__c);
}
}
まとめ
- 外部オブジェクトのレコードは、テストクラスでInsertできない
- ロジックのみをテストしたい場合は、レコードインスタンス生成して引数として渡す方法が有効
- SOQLをテストする場合は、SoqlStubProviderが必須
本記事が、外部オブジェクトのテストで悩んでいる方の参考になれば幸いです!