はじめに
AppExchangeアプリケーションではユーザがカスタマイズできるようにApexクラスをglobalで公開していることがあります。
例えば、ZuoraのOrder BuilderはZuoraのSOAP APIのApex SDKです。
大変便利なのですが、これを使ったクラスのテストはどう書くのでしょうか?
ドキュメントにも(SeeAllData=true)
つけろとしか書いていないし、コミュニティを検索してもこんな解決方法しか見つかりません。これはテストしてるって言いません。
Apex Stub API
ApexでもSpring'17からスタブが使えるようになったので、これで解決してみました。
例
テスト対象クラス
こんなZuoraにクエリを投げるだけの処理のテストコードを書いてみましょう。
ZuoraApiWrapper
クラス(後述)のインスタンスをテスト中はzApiStub
というstatic変数を使うようにしています。
public with sharing class ZuoraExample {
@TestVisible
private static ZuoraApiWrapper zApiStub;
public static String getBankTransferId() {
// テスト内ではzApiStubを利用する
ZuoraApiWrapper zApiInstance = Test.isRunningTest() ? zApiStub : new ZuoraApiWrapper();
zApiInstance.zlogin();
String zoql = 'SELECT Id FROM PaymentMethod WHERE Type=\'BankTransfer\' AND Active = true';
List<Zuora.zObject> methods = zApiInstance.zquery(zoql);
if (methods.isEmpty()) {
throw new NoDataFoundException('ZuoraにBankTransferのPaymentMethodが見つかりません。');
}
return (String) methods.get(0).getValue('Id');
}
}
スタブが作成できない問題の回避のためのクラス
管理パッケージ内のクラスであるZuora.zApi
はスタブを作成できないらしく素直にZuora.zApi
のスタブを作ろうとするとエラーになります。なので、仕方なくラッパークラスを作ることで回避します。
public class ZuoraApiWrapper {
private Zuora.zApi api;
public ZuoraApiWrapper() {
api = new Zuora.zApi();
}
public Zuora.zApi.LoginResult zlogin() {
return api.zlogin();
}
public List<Zuora.zObject> zquery(String query) {
return api.zquery(query);
}
}
テストコード
テストコードはStubProvider
を使ってこんな風に書けます。
もうちょっと頑張れば引数がこれだったらこれを返すとか2回目の呼び出しだったらこれを返すみたいにも書けるようにできると思います。そのレベルのものも標準クラスで提供して欲しいものですが…
@isTest
private class ZuoraExampleTest {
/**
* 呼ばれたメソッドの情報
*/
class MethodInformation {
Object stubbedObject{get;set;}
String stubbedMethodName{get;set;}
Type returnType{get;set;}
List<Type> listOfParamTypes{get;set;}
List<String> listOfParamNames{get;set;}
List<Object> listOfArgs{get;set;}
}
/**
* Stub APIの実装
* https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_testing_stub_api.htm
*/
class ZApiStubProvider implements System.StubProvider {
Map<String, MethodInformation> calledMethod = new Map<String, MethodInformation>();
Map<String, Object> returnMap = new Map<String, Object>();
Map<String, Exception> exceptionMap = new Map<String, Exception>();
public Object handleMethodCall(
Object stubbedObject,
String stubbedMethodName,
Type returnType,
List<Type> listOfParamTypes,
List<String> listOfParamNames,
List<Object> listOfArgs
) {
MethodInformation method = new MethodInformation();
method.stubbedObject = stubbedObject;
method.stubbedMethodName = stubbedMethodName;
method.returnType = returnType;
method.listOfParamTypes = listOfParamTypes;
method.listOfParamNames = listOfParamNames;
method.listOfArgs = listOfArgs;
calledMethod.put(stubbedMethodName, method);
Object res = returnMap.get(stubbedMethodName);
if (res != null)
return res;
Exception e = exceptionMap.get(stubbedMethodName);
if (e != null)
throw e;
return null;
}
ZuoraApiWrapper createStub() {
return (ZuoraApiWrapper) Test.createStub(ZuoraApiWrapper.class, this);
}
/**
* メソッドの戻り値を設定
*/
void setReturnValue(String methodName, Object res) {
returnMap.put(methodName, res);
}
/**
* メソッドが投げるExceptionを設定
*/
void setException(String methodName, Exception e) {
exceptionMap.put(methodName, e);
}
/**
* 呼ばれたメソッドの情報を取得。アサーションに使用する。
*/
MethodInformation called(String methodName) {
return calledMethod.get(methodName);
}
}
/**
* 正常時
*/
@isTest(SeeAllData=true)
static void testSuccess() {
ZApiStubProvider provider = new ZApiStubProvider();
// zqueryメソッドで正しい戻り値を返すよう設定
Zuora.zObject paymentMethod = new Zuora.zObject('PaymentMethod');
paymentMethod.setValue('Id', 'BT0001');
provider.setReturnValue('zquery', new List<Zuora.zObject>{paymentMethod});
// スタブを使うよう設定
ZuoraExample.zApiStub = provider.createStub();
String bankId = ZuoraExample.getBankTransferId();
// zqueryが呼ばれること
System.assertNotEquals(null, provider.called('zquery'));
// 戻り値が正しいこと
System.assertEquals('BT0001', bankId);
}
/**
* 0件の場合
*/
@isTest(SeeAllData=true)
static void testNoData() {
ZApiStubProvider provider = new ZApiStubProvider();
// zqueryメソッドで空のリストを返すよう設定
provider.setReturnValue('zquery', new List<Zuora.zObject>{});
// スタブを使うよう設定
ZuoraExample.zApiStub = provider.createStub();
Exception ex;
try {
ZuoraExample.getBankTransferId();
} catch (NoDataFoundException e) {
ex = e;
}
// zqueryが呼ばれること
System.assertNotEquals(null, provider.called('zquery'));
// NoDataFoundExceptionが発生すること
System.assertEquals('ZuoraにBankTransferのPaymentMethodが見つかりません。', ex.getMessage());
}
}
問題
- ZuoraApiWrapperのカバレッジが0%になる。
- ここでは書きませんが、この問題はうまいこと回避してください。
終わりに
Apexではテストのしようがないみたいなこともけっこうあったので、そんなときにStub APIはなかなか使えそうです。