Apexの自動テストで困った話と解決策


はじめに 

この記事はTeamSpiritアドベントカレンダー2018の記事です。
元JavaエンジニアがApexの自動テストで苦労した点と解決策について書いてみました。

アーキテクチャ

今回題材にするプロジェクトはレイヤードアーキテクチャを採用していて、以下の様な構成になっています。
よく見る形ですね。

テストケース

今回はアプリケーション層のテストについて書いていきます。
アプリケーション層のテストでは、インフラ層の実装に影響を受けることなく、対象メソッドのロジックを検証でき、且つテストコードはなるべくシンプルに保ちたいです。

まずアプリケーション層の処理です。
1.リクエストの入力チェック
2.リクエストに応じてインフラ層からデータを取得
3.取得したデータをレスポンス

次にテストの内容です。
1.リクエストを正しく受け取れているか、リクエストの項目名・型が誤っていないか
2.リクエストが不正な場合、正しくハンドリングできているか
3.リクエストで受け取った値を、正しくインフラ層のメソッドに渡しているか
4.取得したデータを正しくレスポンスしているか
 
簡単な内容ですね。
こういった処理の場合、テストコードではインフラ層のメソッドを適当にモック化して、
アプリケーション層のレスポンスを検証したいですが、Apexではこの様なテストコードを簡単に書くことが出来ません。
いくつかの問題について、追って説明します。


問題1 Apexには便利なMock機能がない

Javaではお馴染みの、MockitoやEazyMock相当のライブラリがApexにはありません。
そのかわり、StubProvider という機能が提供されているので、これを使って解決しましょう。
ただし、そのままでは使いづらいためStubProviderをラップしたクラスを作成します。

Mockクラスの実装次第では、以下の様にテストコードを書けそうです。
(後追いで実装してみたもの → https://github.com/dsk-sgx/ApexMock

Test.cls
@isTest
private static void searchByOrderIdTest() {

  //------------------------
  // ここからがモックの処理です
  //------------------------
  // モックの戻り値を作成します
  Order order = new Order();
  order.id = order01;
  order.item = item01
  order.quantity = 20;
  order.price = 15000;

  // モッククラスを作成します
  Repository mock = (Repository)ApexMock.create(Repository.class);
  //searchOrdersが実行されたら、予め作成したorderを返却するように設定します
  ApexMock.whenCalled(searchOrders).thenReturn(order);
  //------------------------
  // ここまでがモックの処理です
  //------------------------

  // この例ではコンストラクタでモッククラスを渡す形にしています。
  // 以降は通常のテストと同様です。
  OrderApplication app = new OrderApplication(mock);
  // リクエスト作成
  Request request = new Request();
  request.id = order01;
  request.type = DVD;

  // 戻り値が正しくレスポンスに設定しているかを検証
  Response response = app.search(request);
  System.assertEquals(item01, response.orders[0].itemCode);
  System.assertEquals(20, response.orders[0].quantity);
  System.assertEquals(15000, response.orders[0].price);

  // 想定通りにメソッドが実行されていることを検証
  ApexMock.verify(mock)
        .called('searchOrders')  // searchOrdersが実行されていること
        .times(1)                // メソッドが1度だけ実行されていること
        .with('order01', 'DVD'); // 想定した引数で実行されていること
}


問題2 MockでApexのIdクラスを使用している場合、適当な値のIdを作成できない

ApexにはDBのレコードIDを表すIDクラスというものがあり、適切ではない値でインスタンスを作成できません。
これは設計にもよりますが、例えば上記のOrderクラスのidフィールドがId型の場合 order.id = ‘order01’とした時点で実行例外が発生してしまうので、モックのオブジェクトを作成できません。

解決方法
Idクラスを任意の値で作成出来るクラスを実装します。
Idクラスはオブジェクト(DBのテーブル)ごとにprefixが決まっているので、そこも含めて疑似のIdを生成します。

DummyId.cls
@isTest
public class DummyId {
  private static Integer sequence = 0;

  /**
   * オブジェクトごとのダミーIDを作成します。
   * @param type 生成するSObjectの型情報
   * @return 生成したダミーID
   */
  public static Id of(SObjectType type) {
    String cnt = String.valueOf(sequence++);
    String sufix = '0'.repeat(15 - cnt.length()) + cnt;
    return objType.getDescribe().getKeyPrefix() + sufix;
  }
}

このクラスを利用すると先述のコードは以下の様にId型の値を作成することが出来ます。

Test.cls 一部抜粋
  // Order__cオブジェクトのIdを作成します
  Id dummyOrderId = DummyId.of(Order__c.sObjectType);
  Order order = new Order();
  order.id = dummyOrderId;

  // レスポンスと比較します
  Response response = app.search(request);
  System.assertEquals(dummyOrderId, response.orders[0].itemCode);

まとめ

他にも外部ファイルを読み込めなかったり、テストデータ登録時にもガバナ制限を意識する必要があったり制約は残りますが、Salesforceは年に3回リリースをしているため、今後も機能が拡充されていることが期待できます。

簡単にテストして、簡単に品質を担保しましょう。
それでは皆様、素敵なクリスマスを!!