ApexEloquent
コーディングも、テスト作成も、AIが肩代わりする。最後に立ちはだかる物理の壁。それが、テストの実行時間。
要件定義は人間、その先はAIとツールが次々に肩代わりする。 だが「テストを走らせる」一手だけは、DMLという物理に縛られて速くならない。 モデルが何世代進んでも、1msも縮まない。ApexEloquent は、その壁を構造で外します。DI ひとつで。
// DB に触れない単体テストIEntry opp = MockEntry.of(Opportunity.class) .autoId(1) .set('Amount', 50000000); MockEloquent mock = (new MockEloquent()) .attach('fetch', new List<IEntry>{ opp }); new PriceBandUsecase(mock).invoke(); Assert.areEqual('Large', ((Opportunity) mock .upsertedRecordsAt('update')[0]).PriceBand__c);モックに差し込み、Usecase をそのまま呼び、 書かれた結果を取り出す。 この間、データベースは一度も動きません。 AI がテストを量産しても、走らせるコストが積み上がらないのはここが理由です。
The wall AI can't move
テストがレコードを作り、更新する。そのたびにトリガーやフロー、ロールアップが 連鎖して動きます。でも、それらはあくまでプラットフォーム上に組んだ仕組み。 本当に遅いのは、その裏で何度も走るデータベースへの読み書き、 つまりディスクへの物理的な往復です。モデルが賢くなっても、ここは 1ms も縮みません。 そして厄介なのは、テストを書くコストが ゼロに近づくほど、テストを走らせるコストだけが 唯一の律速として残ることです。
機能が増えるほどテストが増える。AI はこれを加速させる側。
必須項目や入力規則を 1 つ足すと、そのオブジェクトを触る既存全テストに課税される。
RunLocalTests は総テスト数 × デプロイ頻度で効いてくる。
3 つが掛かるので、実行時間は線形では済まず、組織の成長に対して超線形に伸びます。 ApexEloquent は DI で、この支配項であるデータベースへの往復を単体テストから丸ごと外し、 インメモリ実行に置き換えます。「少し速くなる」話ではありません。組織が育つほど実行時間を膨らませていた最大の要因が、まるごと消えます。
DB-less tests are fast.They're usually blind, too. Not here.
速さは、たいてい盲目とセットで売られます。ApexEloquent は違う。 速さを主張する以上、その速いテストが本物のバグを捕まえることまで示します。
速さ
同じ 1 テストメソッドの
実行時間 (中央値)
DBレス (MockEloquent)
15ms
vs
実 DML
272〜1,062ms
実運用中の 2 組織で計測。単体層は DML を流さないので、 検証ルールや関連オブジェクト、フローが増えて伸びるのは実 DML 側だけです。
捕まえる力
埋めた 334 個のバグを
見つけられた割合
DBレス (MockEloquent)
83.4%
vs
実 DML
79.3%
ミューテーションテスト (コードにわざとバグを埋め、テストが気づけるか調べる手法) で計測。 速くした代わりに検出力が落ちる、ということは起きませんでした。
いずれも実測ですが、規模は大きくありません (対象クラス 1 つ・各 1 ラン)。 「証明した」ではなく「測ってみたらこうだった」として読んでください。
とはいえ、数字が出ても腑に落ちない点が 2 つ残るはずです。 自分で引っかかったところなので、先に書いておきます。
本物のバグを捕まえる制約 = SELECT 句を、ApexEloquent はモック層に作り直しています。Scribe の設計図(フィールド・リレーション・alias)が モックに丸ごと強制され、SELECT していない項目へのアクセスは単体テストの時点で例外になる。 偽陽性は出荷前に死にます。 SELECT 漏れの安全網 →
成熟した組織では、何が「関係する」かはコードだけで決まりません。 レコードを作る・更新するたびにトリガーやフロー、ロールアップが連鎖し、影響範囲はコード+メタデータ+データが実行時に決める。だから「関係するテスト」の集合は原理的に列挙できない。 対象を絞る運用は、リスクを消すのではなく「遅さ(可視)」を「見逃し(不可視)」に付け替え、"緑" の意味をこっそり縮めているだけです。 ApexEloquent の答えは逆です。対象を絞らず全部走らせて、その時間を大きく下げる。 緑の意味を保ったまま、「全クラスがグリーン = 問題なし」と言い切れる状態を維持します。
Batteries included
テストを書くのに必要なものが、1 つのライブラリに丸ごと入っています。 クエリを組み立てる ORM から、テストデータの生成、実行のモック、Spy、失敗の注入まで。 足りないぶんを別のライブラリや自前のヘルパーで埋める必要がありません。
型付きチェーンで SOQL を組み立てる Scribe。 SELECT・WHERE・並び替えに加え、親項目・子サブクエリ・多対多・集計・動的条件まで同じ書き方で。
MockEntry で、 数式・ロールアップ・自動採番といった本来書き込めない項目にも値を置けます。 親子関係も、集計結果 (AggregateResult) も同じ形で。
取得系 4 種と DML 4 種、外部 ID による upsert までMockEloquent が受けます。 呼び出しサイトにラベルを付ければ、1 本のモックで用途別に仕分けられます。
どのレコードが、どのラベルで保存・削除されたかを後から取り出して検証できます。 実 DML を流さずに「何が起きたか」を見られます。
例外を狙った 1 箇所だけに注入できます。 リトライして成功する経路も、レコード単位の部分保存失敗も、org を細工せずに再現できます。
SELECT していない項目に触れば単体テストで例外。 モックの差し込み先を間違えても例外。何も検証していないのに緑、を構造で塞ぎます。
なお本番の実行は、v3 系から既定がユーザーモード(実行ユーザーの項目・オブジェクト権限を尊重) です。集計や焼き付けのように 「誰が起こしても完遂すべき処理」だけ、systemMode() で明示的に外します。
800+
ライブラリ自身のテスト
テストのためのライブラリが壊れていては話にならないので、ApexEloquent 自身も 800 を超えるテストで固めています。 加えて、受託開発の現場で 10 以上のプロジェクトに導入し、本番運用しています。 作り手が自分の仕事で毎日使い続けているライブラリです。
How it reads
ここまでの道具が、実際のコードではこう見えます。 本番コードと、その単体テスト。 同じ IEloquent が、 本番では実 SOQL を、テストではインメモリを実行します。 色のついた行が、両者が噛み合う継ぎ目です。
テストは、本番が読むラベル 'fetch' に モックレコードを差し込みます。
本番が 'update' に 書いた内容を spy から取り出し、そのまま検証します。
差し替わるのは eloquent の中身だけ (DI で注入)。 Usecase のコードは 1 行も変わりません。
Scribe scribe = Scribe.of(Opportunity.class) .field('Name') .field('Amount') .whereEqual('StageName', 'Prospecting'); List<IEntry> opps = this.eloquent.label('fetch').get(scribe); for (IEntry opp : opps) { Decimal amount = (Decimal) opp.get('Amount'); opp.put('PriceBand__c', amount >= 10000000 ? 'Large' : 'Standard');} this.eloquent.label('update').doUpdate(opps);IEntry opp = MockEntry.of(Opportunity.class) .autoId(1) .set('Name', 'Acme Renewal') .set('Amount', 50000000); MockEloquent mock = (new MockEloquent()) .attach('fetch', new List<IEntry>{ opp }); new PriceBandUsecase(mock).invoke(); List<SObject> updated = mock.upsertedRecordsAt('update');Assert.areEqual('Large', ((Opportunity) updated[0]).PriceBand__c);Why it's one library
クエリビルダ、実行のモック、テストデータ生成。責務が違うのだから、 通常はそれぞれ別のライブラリに分けるべきものです。 それでも 1 つにまとめているのには、理由があります。
SELECT 漏れ検知が、3 点を貫通して初めて成立するからです。
Scribe が「このクエリは何を SELECT したか」を組み立てる
Eloquent /MockEloquent が、その宣言を結果に載せて返す
IEntry がアクセスのたびに突き合わせ、 宣言に無い項目なら例外を投げる
鍵は 3 つめです。照合が走るのは項目にアクセスした瞬間で、 そこに自分のコードを挟めるのは、利用者が触れる型を自分が持っている場合だけです。IEntry を返しているからget() が挟まり、そこで突き合わせられます。 素の SObject を返すと、record.Industry は言語のプリミティブになり、 ライブラリのコードは 1 行も走りません。機能として作らなかったのではなく、挟まる隙間が無いのです。
保証は、利用者が触れる型を自分が持っている範囲でしか作れない。
素の SObject をそのまま返せば、既存コードや標準 API と 何も変換せずに繋がり、覚える型も増えません。ラッパーを返せば、その代わりに アクセスの瞬間へ保証をかけられます。どちらが優れているという話ではなく、 何を取るかのトレードオフです。ApexEloquent は後者を取り、 そのために宣言から照合までを 1 つに収めています。
Position in Apex Stem
ApexEloquent は Apex Stem を構成する 4 つの OSS のうち Data Access を担います。Handler-Usecase Architecture の Usecase 層で DB アクセスが関わるすべての場面に登場し、テスト戦略 の 「Usecase 単体テスト」側を引き受けます。
- Handler-Usecase Architecture: ApexEloquent が呼ばれる Usecase 層の責務
- Layered Constructor Pattern: Usecase に
IEloquentを DI する設計 - Test Strategy:
MockEloquentを軸にした Usecase 単体テスト - Apex Stem Introduction Guide: 動くコード付きの導入 4 ステップ
Start with the Developer Guide
Scribe でのクエリ構築、データ取得と DML、リレーション。 3 つの利用パスを実コードで辿ります。