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 (インターフェース)

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

ラベリング系

シグネチャ戻り値用途
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()IEloquentSOQL / DML を AccessLevel.USER_MODE で実行するコピーを返す (v3 系の既定)
systemMode()IEloquentSOQL / DML を AccessLevel.SYSTEM_MODE で実行するコピーを返す。FLS・オブジェクト権限を無視する

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

APEX
// system プロセス: 呼び出し元クラスを without sharing にしたうえで明示する
this.eloquent.systemMode().label(LBL_UPDATE).doUpdate(entries);
  • 戻り値を使う (v3.9.0+): 呼び出し元のインスタンスは書き換えず、モードを切り替えたコピーを返します。eloquent.systemMode(); と単独の文で呼んでも何も変わらないので、必ず eloquent.systemMode().doX(...) のように戻り値に続けるか、受け直してください。共有している Eloquent が 1 回の systemMode() で以降ずっとシステムモードに化ける事故を防ぐための設計です。直前の label('x') はコピーに引き継がれます (label('x').systemMode().doX() も動きます)
  • MockEloquent では no-op: 自身をそのまま返すだけなので、単体テストはモードを意識せずに書けます (Spy やアサーションが、テストの持っている 1 つのインスタンスを指し続けるようにするため)
  • ⚠️ 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(IEntry entry)IEntry
doInsert(List<SObject> records)List<SObject>
doInsert(List<IEntry> entries)List<IEntry>
doInsert(SObject record, Boolean allOrNone)Database.SaveResult
doInsert(IEntry entry, Boolean allOrNone)Database.SaveResult
doInsert(List<SObject> records, Boolean allOrNone)List<Database.SaveResult>
doInsert(List<IEntry> entries, Boolean allOrNone)List<Database.SaveResult>
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>
doUpsert(SObject record, Boolean allOrNone)Database.UpsertResult
doUpsert(IEntry entry, Boolean allOrNone)Database.UpsertResult
doUpsert(List<SObject> records, Boolean allOrNone)List<Database.UpsertResult>
doUpsert(List<IEntry> entries, Boolean allOrNone)List<Database.UpsertResult>
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
doDelete(SObject record, Boolean allOrNone)Database.DeleteResult
doDelete(IEntry entry, Boolean allOrNone)Database.DeleteResult
doDelete(List<SObject> records, Boolean allOrNone)List<Database.DeleteResult>
doDelete(List<IEntry> entries, Boolean allOrNone)List<Database.DeleteResult>

4 メソッドとも「SObject / IEntry × 単件 / リスト」の 4 形と、それぞれの allOrNone 付き版を持ちます (v3.10.0+ / v2.8.0+。それ以前は doInsert に IEntry 版と allOrNone 版が無く、doUpsert / doDelete にも allOrNone 版がありませんでした)。bulk 化を基本とし、単件版は処理対象が常に 1 件のときだけ使います。

doInsert(IEntry) は、呼び出し元の entry に採番された Id を書き戻しません。Id は戻り値の IEntry から読みます。allOrNone 引数には true / false を明示的に渡してください。null は例外になります (本番の Database API も null の Boolean で落ちるため)。

戻り値の順序 (v3.6.0+ / v2.5.0+): リストを受け取る DML メソッドはすべて、レコードと結果を呼び出し元の入力順で返します。results[i] は件数に関係なく常に records[i] / entries[i] に対応します。以前のバージョンは 10 件以上のリストを内部で並べ替えており、この添字対応が静かに壊れていました。1 つのリストに複数の SObject 型を混在させる場合は、同じ型のレコードをあらかじめ隣接するようにまとめてください。Salesforce は型が切り替わるたびに新しいチャンクを開始し、1 回の DML でチャンクが 10 を超えると明示的なエラーで失敗します。

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

doUpsertByExternalId の用途: 標準 Database.upsert(records, externalIdField, allOrNone) の API を IEloquent 抽象の下に取り込んだもの。ETL / 増分同期 / バッチマイグレーションで必須となる「外部 ID キーによる upsert」を、本番 / Mock 共通の契約で扱えます。キーに指定できるのは、isExternalId() か isIdLookup() が true の項目だけです (Id は常に可)。Account.Name や Product2.ProductCode のような項目は本番の Database.upsert が拒否し、v3.10.0+ / v2.8.0+ の MockEloquent も同じく拒否します。

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, ...) で与えます。

MockEloquent は Scribe の 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 レコード累積
upsertedEntriesAt(String label)List<IEntry>upsertedRecordsAt と同じ中身を IEntry で返す (v3.9.0+ / v2.7.0+)
deletedCountAt(String label)Integer指定ラベルの delete 件数累計
scribeAt(String label)Scribe指定ラベルで発行されたクエリ。1 本も無ければ null、複数あれば例外 (v3.11.0+ / v2.9.0+)
scribesAt(String label)List<Scribe>指定ラベルで発行されたクエリを実行順にすべて (v3.11.0+ / v2.9.0+)
lastScribe()Scribeラベルを問わず最後に発行されたクエリ (v3.11.0+ / v2.9.0+)
attach(String label, IEntry entry)MockEloquent指定ラベルのクエリ返却データとして 1 件をプリロード (チェーン可能、同じラベルへの再 attach は上書き)
attach(String label, List<IEntry> entries)MockEloquent同上、リスト版
failSave(Id recordId, String errorMessage)MockEloquent指定 Id のレコードだけ 保存失敗 させる (チェーン可能)
failSaveByExternalId(String externalIdValue, String errorMessage)MockEloquentdoUpsertByExternalId で、外部 ID 項目の値が一致する行だけ保存失敗させる (v3.9.0+ / v2.7.0+)
failSaveWhen(Schema.SObjectField field, Object value, String errorMessage)MockEloquent指定項目の値が一致する行だけ保存失敗させる (v3.10.0+ / v2.8.0+)
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' ソースに供給されます。

failSave() で部分失敗を作る

allOrNone 付きの DML (doInsert / doUpdate / doUpsert / doUpsertByExternalId / doDelete の allOrNone 版) の SaveResult / UpsertResult / DeleteResult を、レコード単位で失敗にできます。対象は 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 なしで検証できます。

Id を持たない行を失敗させる: failSave は Id で指名するので、まだ Id の無い行 (doInsert(..., false) の新規行や、doUpsert(..., false) の作成側、doUpsertByExternalId で org にまだ無い行) には届きません。この場合は値で指名します。

APEX
// doUpsertByExternalId: 外部 ID 項目の値で指名する (v3.9.0+ / v2.7.0+)
MockEloquent mock = (new MockEloquent())
  .failSaveByExternalId('EXT-002', '重複で拒否されました');
 
// 任意の DML: 任意の項目の値で指名する (v3.10.0+ / v2.8.0+)
MockEloquent mock2 = (new MockEloquent())
  .failSaveWhen(Account.Name, 'doomed', '入力規則で拒否されました');
List<Database.SaveResult> results = mock2.doInsert(records, false);
// Name が 'doomed' の行だけ isSuccess() == false、他の行は保存される
  • 1 行に複数の指定が当たる場合は、failSave (Id) → failSaveByExternalId → failSaveWhen の順で判定します (登録順ではありません)
  • failSaveWhen の比較は Apex の == です。文字列は大文字小文字を区別せず、null を渡すと「その項目が未設定の行すべて」に当たります
  • 失敗結果の getStatusCode() は固定値 (FIELD_CUSTOM_VALIDATION_EXCEPTION、削除は DELETE_FAILED) です。ステータスコードで分岐するコードは、failSave 系では検証できません

クエリの形を検証する (scribeAt + assertCondition)

v3.11.0+ / v2.9.0+。MockEloquent は WHERE を評価せず、attach したデータをそのまま返します。そのため、入力で絞り込むのを忘れたクエリでも、テストにはぴったり正しいデータが返り、緑になります。本番だけが org の該当レコードをすべて触ることになり、モックデータをいくら整えてもこれは暴けません。

この穴を塞ぐのが、発行されたクエリの記録 (scribeAt / scribesAt / lastScribe) と、Scribe 側の条件アサーションです。

APEX
MockEloquent mock = (new MockEloquent()).attach(MyUsecase.LBL_FETCH, contacts);
(new MyUsecase(contactIds, mock)).invoke();
 
mock.scribeAt(MyUsecase.LBL_FETCH)
  .assertCondition(new InConditionClause('Id', contactIds))          // 入力の Id で絞っている
  .assertNoCondition(new EqualConditionClause('Archived__c', false)); // 広げる条件が付いていない
Scribe のメソッド (すべて @TestVisible)用途
assertCondition(clause) / assertNoCondition(clause)クエリ対象オブジェクトの条件 (whereGroup の中も含む) を検証する
assertConditionAt(path, clause) / assertNoConditionAt(path, clause)パスで名指しした場所の条件を検証する
hasCondition(clause) / hasConditionAt(path, clause)同じ判定を Boolean で返す (独自メッセージ用)
conditionAssertionMessage(clause, shouldExist)アサーション失敗時に出るメッセージそのもの

期待値は普通の条件句 (new InConditionClause('Id', ids) / new EqualConditionClause('Type', 'Partner') など) で書きます。項目・演算子・値が一度に固定され、両辺が同じメタデータから組まれるので、toSoql() の文字列比較にはなりません。

条件句は Scribe の where* と 1 対 1 に対応します。 本番コードが使ったメソッドに合わせて選んでください。

本番コードの書き方期待値に使う条件句
whereEqual(f, v)EqualConditionClause
whereNotEqual(f, v)NotEqualConditionClause
whereEqual(f, null) / whereNull(f)IsNullConditionClause
whereNotEqual(f, null) / whereNotNull(f)IsNotNullConditionClause
whereIn(f, values) / whereNotIn(f, values)InConditionClause / NotInConditionClause
whereIn(f, scribe) / whereNotIn(f, scribe) (半結合)InSubQueryConditionClause / NotInSubQueryConditionClause
whereGreaterThan / whereGreaterThanOrEqualGreaterThanConditionClause / GreaterThanOrEqualConditionClause
whereLessThan / whereLessThanOrEqualLessThanConditionClause / LessThanOrEqualConditionClause
whereLike / whereNotLikeLikeConditionClause / NotLikeConditionClause
whereIncludes / whereExcludes (複数選択リスト)IncludesConditionClause / ExcludeConditionClause
  • コンストラクタはどれもテスト専用 (@TestVisible) で、(項目, 値) と (オブジェクト.class, 項目, 値) の 2 形です。値の部分は、In / NotIn がコレクション、半結合が Scribe、Includes / Excludes が List<String>、IsNull / IsNotNull は値なし ((項目) / (オブジェクト.class, 項目)) です
  • ⚠️ null との比較は別の句になります。whereEqual('Email', null) は IsNullConditionClause で組まれるので、期待値も new IsNullConditionClause('Email') で書きます。new EqualConditionClause('Email', null) では一致しません
  • 半結合の内側の条件は、assertConditionAt(項目名, ...) で whereIn に渡した項目名をパスにして検証します
  • パス: 親は Scribe.asParent(...) に渡した項目名、子は relationName(...) (無ければ子オブジェクト名)、半結合は whereIn(field, Scribe) に渡した項目名で 1 段ずつたどります (例: assertConditionAt('ParentId.ParentId', ...))。whereGroup は段に数えません
  • 親の条件は new EqualConditionClause(Account.class, 'Type', 'Partner') のようにオブジェクトを名乗れば、場所を探して検証します。同じオブジェクトが 2 箇所以上にあるときは推測せず例外になるので、パスで名指しします
  • 検証しているのは「条件を持っていること」であって「それで絞れていること」ではありません。orCondition() でつないだ条件も一致します。全件側に広がるのを防ぎたいときは、広げる側の条件を assertNoCondition で禁止します
  • Set と List は区別されます。Set は並び順を正規化して比較し、List は書いた順のまま比較するので、本番コードが渡す型に揃えてください
  • 空コレクションも正当な期待値です。ignoreWhen(ids.isEmpty()) で条件ごと取り下げた分岐は、assertNoCondition(new InConditionClause('Id', ids)) で「付いていない」ことを検証できます
  • 期待値の項目名を綴り間違えると、クエリのどこにも一致しなかった時点で例外になります (assertNoCondition が永久に通ってしまうのを防ぐため)。HAVING は探索しません
  • 失敗は System.AssertException です。Apex では catch できないので、Act を catch (Exception e) で囲んだテストがアサーションを握り潰すことはありません
  • scribeAt は同じラベルで複数のクエリが記録されていると例外になります。ラベルなしで複数回読む場合や、失敗した読み取りを同じラベルでリトライする場合 (クエリは発行時点で記録されます) は scribesAt を使います。クエリを撃たない分岐は Assert.isNull(mock.scribeAt(LBL)) で検証できます

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 系 (failSave / failSaveByExternalId / failSaveWhen。呼び出しは成功するが特定レコードだけ保存されない) を加えたものが、MockEloquent で表現できる失敗の全体です。

次に読む