4
2

More than 5 years have passed since last update.

Apex Stub APIを使って管理パッケージ内のglobalクラスを使ったメソッドのテストを書く

Posted at

はじめに

AppExchangeアプリケーションではユーザがカスタマイズできるようにApexクラスをglobalで公開していることがあります。

例えば、ZuoraのOrder BuilderはZuoraのSOAP APIのApex SDKです。
大変便利なのですが、これを使ったクラスのテストはどう書くのでしょうか?

ドキュメントにも(SeeAllData=true)つけろとしか書いていないし、コミュニティを検索してもこんな解決方法しか見つかりません。これはテストしてるって言いません。

Apex Stub API

ApexでもSpring'17からスタブが使えるようになったので、これで解決してみました。

テスト対象クラス

こんなZuoraにクエリを投げるだけの処理のテストコードを書いてみましょう。
ZuoraApiWrapperクラス(後述)のインスタンスをテスト中はzApiStubというstatic変数を使うようにしています。

ZuoraExample.cls
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のスタブを作ろうとするとエラーになります。なので、仕方なくラッパークラスを作ることで回避します。

ZuoraApiWrapper.cls
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回目の呼び出しだったらこれを返すみたいにも書けるようにできると思います。そのレベルのものも標準クラスで提供して欲しいものですが…

ZuoraExampleTest.cls
@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はなかなか使えそうです。

参照

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2