0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TestDataFactoryとSelectorパターンの限界:2つの本質的な問題

Posted at

この内容は私のポートフォリオサイトのブログ記事 The Limitations of TestDataFactory and Selector Pattern: Two Fundamental Problemsの日本語訳です。

TestDataFactoryとSelectorパターンの限界:2つの本質的な問題

🏭 TestDataFactoryについて

Salesforceでは、Apexテストにおいて明示的にテストデータを作成するために、TestDataFactoryの利用が推奨されています。
TestDataFactoryは、ユーティリティクラスでテストデータを一元管理することで、再利用性の向上テストコードの可読性向上を目的とした設計です。

🔄 TestDataFactoryの2つの使い方

便利なTestDataFactoryですが、複雑なデータ構造を扱うテストになってくると、次のような2つの使い方に分かれます:

  1. 複雑なデータ構造そのものを生成するFactoryメソッドを提供する
    例:Account、Opportunity、OpportunityLineItem、Quote、QuoteLineItem などを一括作成する

  2. 必要最小限の構造だけ提供し、テストごとに肉付けしていく
    例:AccountとOpportunityだけ生成し、その後のQuoteなどはテストクラス側で追加する

どちらの方式にも一長一短はありますが、共通する致命的な問題があります:

あらゆるレコード操作がデータベースアクセスを伴う

  • 親レコードを insert
  • 子レコードを親のIDを参照して insert
  • 孫レコードをさらに insert...

この繰り返しにより、テスト実行時にトリガー処理や不要なロジックが毎回実行され、次第にテストクラスの実行時間が長くなる傾向があります。

そして、テストの速度が劣化した開発現場では、やがてこんな最悪の選択が取られるようになります:

「もうまともなテストなんて書けない。75%のカバレッジだけ満たすための最低限のテストだけ書こう」

これこそが、TestDataFactoryの限界が表面化した姿です。
こうして書かれたApexクラスは、ろくに検証されていないか、過剰に重たいテストに依存している状態になってしまいます。


🎯 Selectorパターンについて

Salesforceがもうひとつ公式に推奨しているのが「Selectorパターン」です。

これは一言で言えば、「Repositoryパターンのうち、SELECT処理だけを抽出してまとめたデザイン」です。
オブジェクト単位で必要なSELECT文を集約することで、取得ロジックの再利用性を高めることを目的としています。

先ほど触れたTestDataFactoryの課題を踏まえると、次のように考えるのは自然でしょう:

このセレクタークラスをモックして、SELECT結果だけ差し替えられれば、テストはもっと速くなるんじゃないか?

ところが、このアプローチにも限界があります。

⚠️ Selectorパターンの構造的な問題

Selectorパターンは「オブジェクト中心の再利用」を目指しているものの、現実の開発では次のような事態が頻発します:

  • 似ているけど微妙に異なるクエリがどんどん増えていく
  • その差分を制御するフラグ引数が必要になる

その結果、セレクタークラスは以下のいずれかの問題に直面します:

  • メソッドが爆発して複雑化
  • フラグ地獄により条件分岐が読みづらくなる

🤔 Selectorのモックは現実的か?

実際にSelectorクラスをモックしようとすると、さらなる問題が発生します。

Interfaceベースのモックの限界

一般的には SelectorInterface のようなインターフェースを定義し、それを MockSelector クラスで実装します。
しかし、この設計には以下の課題があります:

  • メソッドを追加するたびにインターフェースの修正が必要
  • すべての実装クラスでそのメソッドに対応しなければならない
  • 使わないメソッドにもダミー実装を書く必要がある

この結果、モックのための無駄なコードが膨らみ、メンテナンス性が悪化します。

virtual + overrideでの差し替えの限界

そこで、「virtualメソッドを使って必要な部分だけオーバーライドしよう」という選択肢も出てきます。

一見良さそうに見えますが、セレクタークラス自体の肥大化や複雑化、コンテキストの喪失といった本質的な問題は解消できません。
この方法では、根本的な構造の問題は残ったままです。

🕳️ 忘れがちな落とし穴:DML操作はどうする?

さらに忘れがちなのが、データ取得(SELECT)以外のDML操作(insert / update / delete)もテストでは時間と副作用の原因になるという点です。

  • insert → トリガーが発火
  • update → バリデーションルールやProcessBuilderが走る
  • delete → 関連レコードの削除やフローが発動

つまり、SELECTだけをモックしても、テスト高速化には不十分なのです。

🚫 DRY原則を鵜呑みにしないで

SalesforceがSelectorパターンを推奨する理由のひとつに、「コードの重複を避けるため(DRY原則)」があります。

ただし、私はここに重要な落とし穴があると考えています。

AクラスとBクラスが同じクエリを使っているから、共通メソッドにまとめよう

ありがちなパターンですね。
しかし、AとBがまったく異なるコンテキストである場合、これは将来的な危険を孕みます。

たとえば、将来Aクラスの仕様だけが変わってクエリを修正したい場合、Bにも影響が及んでバグが発生することがあります。

DRY原則には「コンテキストが同じであること」が前提条件です。
この前提を無視して「似ているからまとめる」という設計は、変更に弱い脆いコードを生んでしまいます。

Salesforceの公式ドキュメントでもこの点はあまり強調されておらず、私は問題だと感じています。


📉 結論:Selectorパターンにも持続性はない

ここまでを整理すると、TestDataFactoryもSelectorパターンも、最初は便利に見えますが、
プロジェクトの成長とともに、保守性・パフォーマンス・拡張性の面で限界に直面します。

  • FactoryはDMLとトリガー地獄で重くなる
  • Selectorは肥大化と過剰な共通化で破綻する

💡 では、どうすればいいか?

答え:Query Delegation Pattern と Apex Eloquent

私がたどり着いた答えは、**「責務を分離し、モックしやすくする設計思想」**です。

Query Delegation Pattern

  • ドメイン層がクエリ構築を担当
  • Repositoryは取得・保存といったI/Oのみに責任を持つ

この構成により、「A用のクエリ」と「B用のクエリ」がたとえ似ていてもドメイン単位で分離されたまま管理できます。
DRY原則も文脈を守りながら適用できるため、変更にも強い設計が可能です。

そしてこのQuery Delegation Patternを実現するために、私が設計・検証を重ねて作り上げたのが Apex Eloquent です。

🎭 最後に

TestDataFactoryとSelectorパターンは、Salesforceの多くのプロジェクトで使われており、公式にも推奨されています
しかし、それが「持続可能である」かどうかは、また別の話です。

もし、あなたのプロジェクトでテスト設計やデータ取得戦略に限界を感じているなら、
Query Delegation PatternApex Eloquentの導入を、ぜひ一度検討してみてください。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?