Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

参照

atskimura
株式会社co-meeetingのCEO。最近はSalesforce上で動くアプリを作ってます。コピペテックはじめました😀
http://www.co-meeting.co.jp/
co-meeting
株式会社co-meetingでは、創業以来スローガンである「Happy Work! Happy Life!」を掲げ、世界中の「働く人」の人生を豊かにするソフトウェアサービスを開発・提供することを目標とすると同時に、社員みんなが楽しく充実した仕事ができる会社を目指しています。
https://www.co-meeting.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away