TestDataFactory と Selector Pattern の限界: 2 つの根本的な問題

8 min read
SalesforceApexTestingArchitectureClean CodeTestDataFactorySelector PatternQuery DelegationApexEloquentTechnical Debt
TestDataFactory と Selector Pattern が抱える構造的な限界を深掘りし、Query Delegation Pattern + ApexEloquent がなぜ持続可能な解になるのかを論じます。

Salesforce では、 Apex テスト内で明示的にテストデータを作るための仕組みとして TestDataFactory の利用が推奨されています。 TestDataFactory はテストデータをユーティリティクラスに集約して、 再利用性を高めながらテストコードの可読性を上げることを目的としています。

🔄 TestDataFactory の 2 つの使い方

TestDataFactory は便利ですが、 複雑なデータ構造を扱おうとすると、 だいたい次の 2 つのスタイルに分かれてきます。

  1. 複雑な構造そのものを生成する Factory メソッドを用意する
    • (例: Account / Opportunity / OpportunityLineItem / Quote / QuoteLineItem をまとめて作る)
  1. 最小限の構造だけを提供し、 残りはテスト側で組み立てる
    • (例: Account と Opportunity だけ用意し、 Quote 以下は各テストで手動追加する)

どちらにもメリット・デメリットはありますが、 共通する重大な弱点 があります。

レコードの挿入はすべて DB を経由する

  • 親レコードを insert
  • 子レコードは親 ID を参照して insert
  • 孫レコードもまた順番に insert ...

この繰り返しに、 データ挿入のたびに走るトリガーや余計な処理が重なって、 テストクラスの実行時間はどんどん伸びていきます。

テストが遅くなると、 開発チームはしばしば最悪の選択をしてしまいます。

「もう十分なテストを書くのは無理。 デプロイ要件の 75% をギリギリ超える程度のテストでよしとしよう。」

これが TestDataFactory の限界の現れ方の一つです。 この症状が出始めたあと作られた Apex クラスは、 テストケースが足りていないか、 もしくは時間ばかり食う重いテストケースばかりになっていきます。

🎯 Selector Pattern について

Salesforce は公式にもう一つのパターンを推奨しています。 それが「Selector Pattern」 です。

平たく言うと、 これは Repository パターンから「SELECT 処理だけ」 を切り出した設計です。 SELECT 文をオブジェクトごとに集約することで、 取得ロジックの再利用性を高めようというのが基本思想です。

先ほどの TestDataFactory の話を踏まえると、 「じゃあこの Selector クラスをモックして、 DB を経由せずに SELECT 結果を差し替えれば、 テストクラスの崩壊を防げるのでは?」 と考えるのは自然な流れです。

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

⚠️ Selector Pattern の構造的な問題

Selector Pattern は「オブジェクト中心の再利用性」 を狙いますが、 現場で実際に起こりがちなのは次のような事態です。

  • 似ているけれど少しずつ違うクエリが増え続ける
  • 微妙な差異を吸収するためにフラグ引数が必要になる

その結果、 Selector クラスは次のどちらかに陥っていきます。

  • メソッドが増殖して複雑化する
  • 条件分岐が不透明な「フラグ地獄」 になる

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

実際に Selector クラスをモックしようとすると、 さらに別の問題に直面します。

インターフェースベースのモッキングの限界

多くの場合、 Selector クラス用に SelectorInterface のようなものを定義し、 MockSelector で実装する形を取ります。 ただしこの設計には次の課題があります。

  • メソッドを追加するたびに、 インターフェース定義の修正が必要
  • すべての実装クラスにそのメソッドへの対応が要求される
  • 使わないメソッドのためにも、 ダミー実装 (空実装や例外送出) を書かなければならない

結果として保守性が悪化し、 「モックのためのコード」 を大量に書くハメになります。

virtual + override で差し替える方式の限界

これを避けるために、 仮想メソッドを使って必要な部分だけ override する方法を考えるかもしれません。 一見スマートに見えますが、 これは前述した Selector クラスの肥大化・複雑化・コンテキストの喪失といった根本的な問題を解決しません。 構造的な問題はそのまま残ります。

🕳️ 見落とされがちな落とし穴: DML 処理もモックされているか?

多くの開発者が見落としているもう一つの観点。 「取得処理だけでなく、 DML 操作 (insert / update / delete) もテストでは実行時間と副作用を生む 」 ということです。

  • insert はトリガーを発火させる
  • update はバリデーションや Process Builder を起動させる
  • delete は関連レコードのカスケード削除やフローを発火させる

つまり、 取得処理だけをモックしても、 テストパフォーマンス改善としては不完全なのです。

🚫 DRY 原則を盲信しない

Salesforce が Selector Pattern を公式に採用している背景には、 「重複コード (コピーペースト) を防ぐため」 という DRY (Don't Repeat Yourself) 原則の実践があります。

ただ、 私はこのアプローチへの過信を強く戒めたいと考えています。

「クラス A とクラス B が同じクエリを使っているので、 一本化しよう。」

よくあるシナリオです。 しかし A と B が 異なる文脈 にあるなら、 この一本化は非常に危険になります。 将来、 A の仕様だけが変わったときに、 B にも意図せず影響が及ぶ可能性があるからです。

DRY 原則はもともと「同じ文脈の中で再利用する 」 ことを前提にしています。 これを無視して「形が似ているから一本化する」 を続けると、 変更に弱いシステムができあがります。

Salesforce 公式のガイドでさえ、 このポイントを十分に強調していないのが個人的に気になっています。

📉 まとめ: Selector Pattern にも持続可能性は無い

ここまでの議論を整理すると、 TestDataFactory も Selector Pattern も、 最初は便利に見えますが、 プロジェクトが成長するにつれて保守性・パフォーマンス・スケーラビリティの面で限界に達してしまいます。

  • Factory は DML とトリガー地獄で重くなる
  • Selector は肥大化と「一本化の落とし穴」 で設計が破綻する

💡 ではどうするか?

解: Query Delegation Pattern と ApexEloquent

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

Query Delegation Pattern

  • ドメイン層がクエリを組み立てる
  • Repository は「取得」 「保存」 のような I/O に専念する

この構成にすると、 SELECT の粒度を「ドメインの目的」 に合わせて設計できるようになります。 「A 用」 と「B 用」 で似たクエリが必要になっても、 別々に持っておけばよい — つまり、 文脈に応じて DRY 原則を適用するか判断できる 余地が生まれます。

この Query Delegation Pattern を実践するために、 実装と検証を繰り返した末にたどり着いたのが ApexEloquent です。

🎭 最後に

TestDataFactory も Selector Pattern も、 Salesforce 公式に推奨されており、 多くのプロジェクトで採用されています。 ただ、 それらが「持続可能か」 となると、 また別の話になります。

もしいまのテスト設計やデータ取得戦略に限界を感じているなら、 Query Delegation Pattern と ApexEloquent の採用を、 一度検討してみてください。