API リファレンス: IEloquent / Eloquent / MockEloquent

Apex Stem ドキュメント
Apex StemApexEloquentAPI ReferenceIEloquentMockEloquentSalesforceApex
IEloquent インターフェースとその実装 Eloquent / MockEloquent の詳細リファレンス。共通契約、Spy プロパティ、failOn シリーズまで網羅します。

IEloquent は ApexEloquent のデータアクセス契約 (インターフェース)。本番では Eloquent、テストでは MockEloquent を Layered Constructor Pattern で DI して使います。

使い方や典型シナリオは データ取得と DML、IEntry、Mock を参照してください。

このページは v3 系・v2 系の共通内容です。両者の違いは実行モードだけなので、そこだけ「実行モード (v3 系)」として分けて示します。

3 つの関係

CODE
IEloquent  (interface)       ← Usecase が依存する契約
   ↑                       
   ├── Eloquent             ← 本番。標準 SOQL / DML をそのまま叩く
   └── MockEloquent         ← テスト。DB を介さず、Spy + failOn* を提供

Usecase は IEloquent 型のフィールドに本番では new Eloquent()、テストでは new MockEloquent(...) を Layered Constructor で受け取ります。

IEloquent (インターフェース)

すべての契約メソッドは EloquentMockEloquent の両方で実装されています。

ラベリング系

シグネチャ戻り値用途
label(String labelName)IEloquent直後の 1 操作にラベルを付与してチェーン可能にする。1 つの IEloquent を「先月用」「今期用」「Account 更新用」のように ラベルで多重化 できるので、用途ごとに IEloquent を分けて DI する代わりに 1 本にまとめられる
APEX
// 同じ IEloquent で 2 つの SOQL と 1 つの DML を区別する
IEntry job = this.eloquent.label('jobLoad').firstOrFail(jobScribe);
List<IEntry> details = this.eloquent.label('detailLoad').get(detailScribe);
this.eloquent.label('finalDml').doUpdate(toUpdate);

null / 空文字を label(...) に渡すと例外になります。

'default'ラベルなしの操作に内部で割り当てられる名前です。label('default') 自体は通りますが、ラベルなしの呼び出しと同じ枠を指すことになるので、意図が伝わりません。別の名前を付けてください (whenLabel('default') のほうは冗長として明示的に弾かれます)。

opt-in strict mode: IEloquent インスタンスで初めて .label(...) を呼んだ瞬間、そのインスタンスは strict mode にスイッチします (インスタンスの生存中ずっと sticky)。

  • 以降のすべての操作は .label(...) を前置する必要がある (ラベルなし操作は例外)
  • 同じラベルを同じインスタンスで 2 回使うと例外
  • 一度も .label() を呼ばないインスタンスは従来通り (= lenient mode、後方互換)

これは「新しい DML を足したのにラベルを忘れていた」という事故を実行時に塞ぐためのガード。silent に default バケットに溜まる footgun を防ぎます。

実行モード系 (v3 系のみ)

シグネチャ戻り値用途
userMode()IEloquent以降の SOQL / DML を AccessLevel.USER_MODE で実行する (v3 系の既定)
systemMode()IEloquent以降の SOQL / DML を AccessLevel.SYSTEM_MODE で実行する。FLS・オブジェクト権限を無視する

v3 系では Eloquentinherited sharing になり、SOQL / DML の既定が ユーザーモード (実行ユーザーの項目・オブジェクト権限を尊重) に変わりました。集計・焼き付け・データ移行のように「誰が起こしても完遂すべき処理」だけ systemMode() で明示的にオプトアウトします。

APEX
// system プロセス: 呼び出し元クラスを without sharing にしたうえで明示する
this.eloquent.systemMode().label(LBL_UPDATE).doUpdate(entries);
  • sticky: 一度呼ぶと、そのインスタンスの以降すべての操作に効きます (label() のように操作ごとにリセットされません)
  • MockEloquent では no-op: 自身をそのまま返すだけなので、単体テストはモードを意識せずに書けます
  • ⚠️ systemMode() が外すのは FLS・オブジェクト権限だけ です。共有 (レコードの可視性) は別軸で、外すには呼び出し元クラスを without sharing にする必要があります

クエリ系

シグネチャ戻り値用途
get(Scribe scribe)List<IEntry>クエリ実行。0 件なら空リスト
first(Scribe scribe)IEntry先頭 1 件。0 件なら null
firstOrFail(Scribe scribe)IEntry先頭 1 件。0 件なら ApexEloquentException
firstOrFail(Scribe scribe, Exception orFail)IEntry先頭 1 件。0 件なら 渡した例外をそのまま throw する
getAsSObject(Scribe scribe)List<SObject>SObject リスト (最終手段)
firstAsSObject(Scribe scribe)SObject先頭 1 件の SObject 版
firstOrFailAsSObject(Scribe scribe)SObject先頭 1 件の SObject 版、0 件で例外
rawSoql(String soql)List<IEntry>Scribe をバイパスして生 SOQL を実行 (最終手段。SELECT 漏れ検知が無効化される)

firstOrFail(scribe, orFail) の用途: 0 件を業務エラーとして画面に返したいときに使います。渡した例外がそのまま throw されるので、first + null 判定 + 自前 throw を書かずに済みます。

効いてくるのは catch した先です。業務例外を渡しておけば、そのまま catch (UsecaseException) で受けられます。

APEX
IEntry job;
try {
  job = this.eloquent.label(LBL_JOB).firstOrFail(
    jobScribe,
    new UsecaseException('対象のジョブが見つかりませんでした。')
  );
} catch(UsecaseException ex) {
  this.t.skip('業務エラー: ' + ex.getMessage());
}

引数なしの firstOrFail(scribe) だと ApexEloquentException が飛ぶので、この catch には入りません。

DML 系

シグネチャ戻り値
doInsert(SObject record)SObject
doInsert(List<SObject> records)List<SObject>
doUpdate(SObject record)SObject
doUpdate(IEntry entry)IEntry
doUpdate(List<SObject> records)List<SObject>
doUpdate(List<IEntry> entries)List<IEntry>
doUpdate(SObject record, Boolean allOrNone)Database.SaveResult
doUpdate(IEntry entry, Boolean allOrNone)Database.SaveResult
doUpdate(List<SObject> records, Boolean allOrNone)List<Database.SaveResult>
doUpdate(List<IEntry> entries, Boolean allOrNone)List<Database.SaveResult>
doUpsert(SObject record)SObject
doUpsert(IEntry entry)IEntry
doUpsert(List<SObject> records)List<SObject>
doUpsert(List<IEntry> entries)List<IEntry>
doUpsertByExternalId(SObject record, Schema.SObjectField externalIdField, Boolean allOrNone)Database.UpsertResult
doUpsertByExternalId(IEntry entry, Schema.SObjectField externalIdField, Boolean allOrNone)Database.UpsertResult
doUpsertByExternalId(List<SObject> records, Schema.SObjectField externalIdField, Boolean allOrNone)List<Database.UpsertResult>
doUpsertByExternalId(List<IEntry> entries, Schema.SObjectField externalIdField, Boolean allOrNone)List<Database.UpsertResult>
doDelete(SObject record)void
doDelete(IEntry entry)void
doDelete(List<SObject> records)void
doDelete(List<IEntry> entries)void

bulk 化を基本とし、単件版は処理対象が常に 1 件のときだけ使います。

allOrNone 引数付き doUpdate の用途: 戻り値が Database.SaveResult / List<Database.SaveResult> なので、partial-success 時の per-record エラー集約をユニットテストで検証できます。ETL / 増分同期 / バッチマイグレーションで「失敗したレコードだけログに残して続行する」ようなパスがある時に使います。

doUpsertByExternalId の用途: 標準 Database.upsert(records, externalIdField, allOrNone) の API を IEloquent 抽象の下に取り込んだもの。ETL / 増分同期 / バッチマイグレーションで必須となる「外部 ID キーによる upsert」を、本番 / Mock 共通の契約で扱えます。

Eloquent (Production)

IEloquent を実装する本番クラス。標準の SOQL / DML をそのまま叩きます。追加の public メソッドは無し (インターフェース通り)。

APEX
IEloquent eloquent = new Eloquent();
List<IEntry> opps = eloquent.get(scribe);
eloquent.doUpdate(opps);

MockEloquent (Mock 拡張)

IEloquent の本契約に加えて、テスト用の Spy プロパティfailOn シリーズ を持ちます。

コンストラクタ

シグネチャ振る舞い
new MockEloquent()空。ラベルを使う場合は attach(...) でデータを与える
new MockEloquent(IEntry entry)ラベルなし ('default') の返却データとして 1 件をプリロード
new MockEloquent(List<IEntry> entries)同上、リスト版

コンストラクタで渡したデータが供給されるのは ラベルなし ('default') の操作だけです。本番コードが label(...) を使っている場合は、コンストラクタではなく attach(label, ...) で与えます。

MockEloquentScribe の WHERE 条件を評価せず、渡したリストをそのまま返します。条件違いのクエリを区別したい場合は、クエリ単位にラベルを付けて attach(label, ...) で仕分けます (下記)。

未 attach のラベルは例外になる

label('X')get / first / firstOrFail を呼んだのに attach('X', ...) していない場合、テスト実行時は例外になります。エラーには attach 済みのラベル一覧が付くので、打ち間違いはその場で分かります。

これは「ラベル名を間違えた → 0 件が返る → 対象なしでスキップの分岐に入る → 何も検証していないのにテストが緑」という偽陽性を塞ぐためのガードです。

「0 件の経路」を意図してテストしたいときは、空リストを明示的に attach して意図を宣言します。

APEX
MockEloquent mock = (new MockEloquent())
  .attach(MyUsecase.LBL_FETCH, new List<IEntry>());

古いバージョンから上げると、この変更で赤くなるテストが出ることがあります。それは「attach 漏れで何も検証せずに緑だった」テストです。機械的に空 attach を足して緑に戻すのではなく、本来そこに注入すべきだったデータを確認してください。

Spy プロパティとラベル別アクセサ

プロパティ / メソッド中身
upsertedRecordsList<SObject>doInsert / doUpdate / doUpsert / doUpsertByExternalId で渡されたレコードのうち 'default' バケット の累積 (※ @deprecated: 新規コードは upsertedRecordsAt('default') を推奨)
deletedCountIntegerdoDelete の呼び出し件数のうち 'default' バケット の累計 (※ @deprecated: 新規コードは deletedCountAt('default') を推奨)
upsertedRecordsAt(String label)List<SObject>指定ラベルの DML レコード累積
deletedCountAt(String label)Integer指定ラベルの delete 件数累計
attach(String label, IEntry entry)MockEloquent指定ラベルのクエリ返却データとして 1 件をプリロード (チェーン可能、同じラベルへの再 attach は上書き)
attach(String label, List<IEntry> entries)MockEloquent同上、リスト版
failSave(Id recordId, String errorMessage)MockEloquent指定 Id のレコードだけ 保存失敗 させる (チェーン可能)

failSave() で部分失敗を作る

allOrNone 付きの DML (doUpdate(records, false) / doUpsertByExternalId(..., false)) の SaveResult / UpsertResult を、レコード単位で失敗にできます。対象は Id で指名するので、MockEntry.autoId() と対で使います。

APEX
Id badId = MockEntry.of(Account.class).autoId(2).getId();
 
MockEloquent mock = (new MockEloquent())
  .failSave(badId, '入力規則で拒否されました');
  • allOrNone = false — そのレコードの結果が success = false と指定メッセージを持ち、Spy には積まれません (保存されていないため)
  • allOrNone = true — 実際の all-or-nothing DML と同じく、何も記録される前に操作全体が例外になります

「失敗したレコードだけログに残して続行する」パスを、実 DML なしで検証できます。

APEX
// ラベルなしの従来の使い方 (後方互換)
MockEloquent updateEloquent = new MockEloquent();
(new MyUsecase(input, fetchEloquent, updateEloquent)).invoke();
Assert.areEqual(1, updateEloquent.upsertedRecords.size());
Opportunity updated = (Opportunity) updateEloquent.upsertedRecords[0];
Assert.areEqual('Technology', updated.Industry__c);
APEX
// ラベル多重化版 — 1 つの MockEloquent でクエリ返却と DML 検証をラベルで仕分け
MockEloquent mock = (new MockEloquent())
  .attach('jobLoad', jobEntry);
 
(new FinalizeJobUsecase(jobId, mock)).invoke();
 
Assert.areEqual(1, mock.upsertedRecordsAt('finalDml').size());

後方互換性ノート: 既存の upsertedRecords / deletedCount パブリックフィールドは引き続き動作し、内部的に 'default' バケットを反映します。JSDoc 上で @deprecated 化されており、段階的に *At('default') への移行が推奨されます。また、レガシーコンストラクタ new MockEloquent(List<IEntry>) も引き続き動作し、渡されたデータは 'default' ソースに供給されます。

failOn シリーズ (例外シミュレーション)

各メソッドは引数なし版と Exception 受け取り版の 2 オーバーロード。Exception を渡さない場合はデフォルト例外が投げられます。

メソッド対応する契約
failOnGet() / failOnGet(Exception e)get(scribe)
failOnFirst() / failOnFirst(Exception e)first(scribe)
failOnFirstOrFail() / failOnFirstOrFail(Exception e)firstOrFail(scribe)
failOnGetAsSObject() / failOnGetAsSObject(Exception e)getAsSObject(scribe)
failOnFirstAsSObject() / failOnFirstAsSObject(Exception e)firstAsSObject(scribe)
failOnFirstOrFailAsSObject() / failOnFirstOrFailAsSObject(Exception e)firstOrFailAsSObject(scribe)
failOnRawSoql() / failOnRawSoql(Exception e)rawSoql(soql)
failOnDoInsert() / failOnDoInsert(Exception e)doInsert(*)
failOnDoUpdate() / failOnDoUpdate(Exception e)doUpdate(*)
failOnDoUpsert() / failOnDoUpsert(Exception e)doUpsert(*)
failOnDoUpsertByExternalId() / failOnDoUpsertByExternalId(Exception e)doUpsertByExternalId(*)
failOnDoDelete() / failOnDoDelete(Exception e)doDelete(*)

whenLabel() で失敗をラベルにスコープする

シグネチャ用途
whenLabel(String label)直前の failOn*()特定のラベルでのみ発火 するようスコープする (チェーン可能)
APEX
// label('finalDml') の DML だけが失敗する、 他のラベルは正常に動く
MockEloquent mock = (new MockEloquent())
  .failOnDoUpdate(new DmlException('Simulated finalDml failure'))
  .whenLabel('finalDml');

「fail on get when label is X」のように、ラベル多重化 Usecase で「この副作用だけ落としたい」ケースを表現できます。

APEX
MockEloquent mock = (new MockEloquent())
  .failOnDoUpdate(new DmlException('Simulated DML failure'))
  .whenLabel(MyUsecase.LBL_UPDATE);
 
try {
  (new MyUsecase(input, mock)).invoke();
  Assert.fail('Expected exception');
} catch(DmlException e) {
  Assert.isTrue(TraceFlow.isLastAbort());
}

repeat() で「常に失敗」を表現

シグネチャ用途
repeat()直前の failOn*以降ずっと繰り返し たい時に呼ぶ (回数指定はできない)

failOn() 単独だと「次の 1 回だけ失敗」です。failOn同じメソッドに対してキューに積まれる ので、2 回並べれば 1 回目と 2 回目が失敗し 3 回目は成功します (リトライの成功パス)。

.repeat() を付けると、直前に仕掛けた設定が 以降すべての呼び出しで失敗 し続けます。「最大 3 回までリトライ、それでもダメなら中止」というロジックの 中止側のパス を、回数を数えずに確かめられます。

キューは ラベルごとに独立 しています。whenLabel を付けなかった設定は、ラベルなしの呼び出し ('default') 宛てになります。

⚠️ テスト設定のビルダー (attach / failOn* / whenLabel / failSave / repeat) は、いずれも this を変更せず新しいインスタンスを返します。戻り値を受け取らないと設定が消えるので、チェーンでつなぐか受け直してください。

💡 ラベルの「1 度だけ」制約が数えるのは 成功した操作 です。失敗した操作はラベルを解放するため、本番コードが catch して同じラベルでリトライする実装もそのままテストできます。

⚠️ ラベルを使っているテストで whenLabel を付け忘れると、その設定は 'default' 宛てになり永久に発火しません。これは診断メッセージ付きの例外として検出されます (付けるべきラベル名も表示されます)。

表現できる失敗パターン

failOn* / whenLabel / repeat の組み合わせで書ける形は、次の 5 つに整理できます。

書き方起きること
failOnGet(exA).failOnGet(exB)1 回目は exA、2 回目は exB で失敗し、3 回目は成功
failOnGet(ex).repeat()毎回失敗
failOnGet(ex).whenLabel('opp')'opp' の呼び出しが 1 回失敗し、リトライすれば成功
failOnGet(exA).whenLabel('opp')
.failOnGet(exB).whenLabel('opp')
'opp' が 2 回連続で失敗し、3 回目は成功
failOnGet(ex).whenLabel('opp').repeat()'opp' の呼び出しが毎回失敗
APEX
// 「1 回失敗 → リトライで成功」を書く
MockEloquent mock = (new MockEloquent())
  .attach('opp', new List<IEntry>{ oppEntry })
  .failOnGet(new QueryException('boom'))
  .whenLabel('opp');
 
// 1 回目: 例外
// 2 回目: 失敗がラベルを解放しているので、attach したデータが返る

キューはラベルごとに独立しているので、同じメソッドに対してラベル違いの失敗を並べても、互いに干渉しません。

APEX
// 取得は 'fetch' で、保存は 'update' で、それぞれ別の理由で落とす
MockEloquent mock = (new MockEloquent())
  .failOnGet(new QueryException('取得に失敗')).whenLabel('fetch')
  .failOnDoUpdate(new DmlException('保存に失敗')).whenLabel('update');

これらに failSave (呼び出しは成功するが特定レコードだけ保存されない) を加えたものが、MockEloquent で表現できる失敗の全体です。

次に読む