Test.loadData(改)を作ってみた

みなさん、こんにちは。 今回はApexのテストクラスでテストデータを作成する際のお話しをしてみたいと思いますが、みなさんはどのようにテストデータを作成されているでしょうか?

Spring '12でリリースされたAPI 24.0以降では、一部のオブジェクトのデータ以外はテストクラスのメソッド内で作成されたデータにしかアクセスできなくなっているため、さすがにもう組織に存在するデータに依存するような実装はされていないと思います。

恐らく一般的な実装方法としましては、下記の何れか、または両方を適用させた形になるでしょうか。

  • テストデータを作成するための共通クラスを作成し、データ登録を行うメソッドをオブジェクト毎に分けて実装

  • Winter13から利用可能になった「Test.loadData」を使用

ただ、実際にテストクラスの実装をしている際、何かもっと効率的にならんかな、といつも思っておりました。

比較的規模の大きな開発を行う場合、上記の方法だと他の方が作成したテストクラスをレビューしたりメンテナンスする際に、どんなテストデータを登録してテストしているのかを把握するのに結構な時間がかかる場合がありますし、テストデータを作成する共通クラスをメンテナンスした場合に既存のテストクラスへの影響が怖かったりもします。

そんな中、一瞬期待したのがWinter13から利用可能になった「Test.loadData」でした。 静的リソースに登録しているCSVファイルをテストクラスでのみロードできるようにする機能とのことで、これはもしやと期待したのですが、社内の方が調査したところ、オブジェクト間で関連を持たせるようなデータのロードは想定されていないような仕様でした。

多少テストデータを理解しやすい形にはなるので悪い事ではないのですが、トランザクションデータの作成には向かなそうで、「Test.loadData」はマスタデータ等、最上位の階層のオブジェクトのデータを登録するという用途で利用するものかな、というのが私の認識です。

なんとかしたいなーと思いつつも、優先順位的に時間が思うように確保できなかったのですが、空き時間を見つけて試しにポイントとなる部分を実装して検証してみたところ、何とか上手い事いけそうだったので、今回「Test.loadData」のパワーアップ版?のプログラムを書いてみました。

Test.loadData(改) 概要

今回実装したプログラムの基本的な考え方は「Test.loadData」と同様ですが、オブジェクト間で関連付けをした上でデータを登録できるようにしました。

簡単に処理の流れを書きます。    

1. 静的リソースからCSVデータを取得  

2. CSVデータを改行コードで分割  

3. 行データを1文字ずつ読み取りながら列データに分割  

4. オブジェトの項目定義を取得し、列データをオブジェクトの項目にマッピング  

5. オブジェクトの項目が参照項目の場合、親レコードに関連付けするようなデータが指定されていれば親レコードのIDを取得して設定  

6. insert

5の処理以外は「Test.loadData」の処理と恐らくほぼ同様だと思います。 早速下記オブジェクトのデータをロードしてみたいと思います。

  • 取引先(Account)
  • 商談(Opportunity) ※取引先と関連
  • カスタムオブジェクト(CustomObjectA__c) ※取引先と関連

DataLoaderでCSVデータをダウンロード

まずDataLoaderを使用して上記オブジェクトのデータをダウンロードします。 事前にSalesforceの画面でサンプルデータを登録した後にDataLoaderでダウンロードした方がこの後の作業がスムーズにいくかもしれません。 csvダウンロード

取引先のCSVファイルの準備

次にCSVファイルを開き、値の修正を行います。 取引先(Account)のCSVファイルは、修正なしでこのまま使用することにします。 csv_account

注意点として、この後当該ファイルを静的リソースに登録してApex内で読み込むのですが、UTF-8にする必要があります。 一般的なテキストエディタであればエンコーディングの変換は行えますので、そちらを利用してUTF-8に変換を行います。

商談のCSVファイルの準備

商談(Opportunity)のCSVファイルは、取引先と紐づけるのでC列の「ACCOUNTID」に以下のルールで値を設定します。  

関連付けのルール:{静的リソース名:行番号}

「静的リソース名」の部分には、後述しますが「オブジェクトのAPI名_任意の文字列」を指定します。 そして「行番号」の部分には、取引先のCSVファイルのデータ部分(ヘッダ行は除く)の何番目のレコードと紐づけするかを指定します。 csv_opportunity

当該ファイルもエンコーディングをUTF-8に変換します。

カスタムオブジェクトのCSVファイルの準備

カスタムオブジェクト(CustomObjectA__c)のCSVファイルも、取引先と紐づけるのでD列の「ACCOUNT__C」に商談の時と同じルールで値を指定します。 csv_customobject

当該ファイルもエンコーディングをUTF-8に変換します。

静的リソースへの登録

CSVファイルの準備が整いましたので、次はそれぞれ静的リソースに登録します。 ちなみに静的リソース名には、上でも少し触れましたが下記のルールで値を設定します。  

静的リソース名のルール:オブジェクトのAPI名_任意の文字列

取引先のCSVファイル: static_account

商談のCSVファイル: static_opportunity

カスタムオブジェクトのCSVファイル: static_customobject

※カスタムオブジェクトのAPI名のサフィックス「__c」のように連続したアンダーバーを静的リソースの名前に指定することはできませんので、標準オブジェクトのようにサフィックス無しの値を設定します

これでデータの準備が整いましたので、次は実際にデータのロードを実行してみます。

実行

本来ならばtestMethod内で実行する想定ですが、それだと味気ないので開発コンソールから実行してみようと思います。

今回作成したクラスは「TestDataLoader」というもので、loadというstaticなメソッドを実装しました。 引数には、静的リソース名がロードする順番に指定された配列を受け取ります。 戻り値は登録後のオブジェクトのリストを保持したMapとなります。 console

それでは実行結果を見てみたいと思います。

取引先A: record_account1

取引先B: record_account2

どうやら上手い事いったようです。 ちなみにこれが実行後のガバナ情報です。 g

SOQLとDMLの発行、そしてDescribeの取得は1オブジェクトにつきそれぞれ1回ですので、CSVのデータ数にさえ気を付けさえすれば、一般的な用途では特に問題にはならないと思います。 ただ、ステートメント数がやたら多いです。

今回は取引先と商談が2件ずつ、そしてカスタムオブジェクトが3件と大した件数ではありませんが、ステートメント数が既に7768となっています。 これは、値の中にダブルコーテーションやカンマ、そして改行コードが含まれることを想定し、CSVデータを1文字ずつ処理するよう私が実装したのが原因だと思います。

※ちなみに、Winter'14からはステートメント数によるガバナ制限は撤廃されているはずなので、ステートメント数による制限については考慮しなくてもよいかもしれません(その代りに新たに追加されたCPUtimeによるガバナ制限を考慮する必要があります)。詳細はこちらをご参照ください。

純粋に値を設定する項目だけをCSVファイルに出力するようにすれば、もっと読み込めるデータは増えますし、あくまでテストデータとして読み込むのであれば、ダブルコーテーションやカンマ、そして改行コードが含まれることを想定せず、単純にカンマで分割するように処理を分けてもよいかもしれません。 また、もっと効率の良いロジックで実装すればガバナ制限に抵触する可能性を低くすることができるかもしれません。

ただ今回は時間の関係上、ここでタイムアウトです。

最後に

今回は、少しでもテストクラスの実装が効率良くなり、そして少しでもメンテナンスが高まればと思い「Test.loadData」もどきのプログラムを書いてみました。 このページの最後にプログラムを載せますが、時間の関係上コメントも一切なく汚いプログラムで、また簡単なテストしか行っていない為、バグが潜んでる可能性がありますので、ご利用になる場合はご注意頂ければと思います。

それにしても、Test.loadDataでこれが実現できればいいんだけどなあ・・・ insert前のオブジェクトを返してくれるだけでも全然使い勝手が違うのになあ・・・