API リファレンス: IEloquent / Eloquent / MockEloquent
IEloquent は ApexEloquent のデータアクセス契約 (インターフェース)。本番では Eloquent、テストでは MockEloquent を Layered Constructor Pattern で DI して使います。
使い方や典型シナリオは データ取得と DML、IEntry、Mock を参照してください。
このページは v3 系・v2 系の共通内容です。両者の違いは実行モードだけなので、そこだけ「実行モード (v3 系)」として分けて示します。
3 つの関係
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 本にまとめられる |
// 同じ 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 系では Eloquent が inherited sharing になり、SOQL / DML の既定が ユーザーモード (実行ユーザーの項目・オブジェクト権限を尊重) に変わりました。集計・焼き付け・データ移行のように「誰が起こしても完遂すべき処理」だけ systemMode() で明示的にオプトアウトします。
// 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) で受けられます。
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 メソッドは無し (インターフェース通り)。
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 して意図を宣言します。
MockEloquent mock = (new MockEloquent())
.attach(MyUsecase.LBL_FETCH, new List<IEntry>());
古いバージョンから上げると、この変更で赤くなるテストが出ることがあります。それは「attach 漏れで何も検証せずに緑だった」テストです。機械的に空 attach を足して緑に戻すのではなく、本来そこに注入すべきだったデータを確認してください。
Spy プロパティとラベル別アクセサ
| プロパティ / メソッド | 型 | 中身 |
|---|---|---|
upsertedRecords | List<SObject> | doInsert / doUpdate / doUpsert / doUpsertByExternalId で渡されたレコードのうち 'default' バケット の累積 (※ @deprecated: 新規コードは upsertedRecordsAt('default') を推奨) |
deletedCount | Integer | doDelete の呼び出し件数のうち '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) | MockEloquent | doUpsertByExternalId で、外部 ID 項目の値が一致する行だけ保存失敗させる (v3.9.0+ / v2.7.0+) |
failSaveWhen(Schema.SObjectField field, Object value, String errorMessage) | MockEloquent | 指定項目の値が一致する行だけ保存失敗させる (v3.10.0+ / v2.8.0+) |
// ラベルなしの従来の使い方 (後方互換)
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);
// ラベル多重化版 — 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() と対で使います。
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 にまだ無い行) には届きません。この場合は値で指名します。
// 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 側の条件アサーションです。
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 / whereGreaterThanOrEqual | GreaterThanConditionClause / GreaterThanOrEqualConditionClause |
whereLessThan / whereLessThanOrEqual | LessThanConditionClause / LessThanOrEqualConditionClause |
whereLike / whereNotLike | LikeConditionClause / 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*() を 特定のラベルでのみ発火 するようスコープする (チェーン可能) |
// label('finalDml') の DML だけが失敗する、 他のラベルは正常に動く
MockEloquent mock = (new MockEloquent())
.failOnDoUpdate(new DmlException('Simulated finalDml failure'))
.whenLabel('finalDml');
「fail on get when label is X」のように、ラベル多重化 Usecase で「この副作用だけ落としたい」ケースを表現できます。
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' の呼び出しが毎回失敗 |
// 「1 回失敗 → リトライで成功」を書く
MockEloquent mock = (new MockEloquent())
.attach('opp', new List<IEntry>{ oppEntry })
.failOnGet(new QueryException('boom'))
.whenLabel('opp');
// 1 回目: 例外
// 2 回目: 失敗がラベルを解放しているので、attach したデータが返る
キューはラベルごとに独立しているので、同じメソッドに対してラベル違いの失敗を並べても、互いに干渉しません。
// 取得は 'fetch' で、保存は 'update' で、それぞれ別の理由で落とす
MockEloquent mock = (new MockEloquent())
.failOnGet(new QueryException('取得に失敗')).whenLabel('fetch')
.failOnDoUpdate(new DmlException('保存に失敗')).whenLabel('update');
これらに failSave 系 (failSave / failSaveByExternalId / failSaveWhen。呼び出しは成功するが特定レコードだけ保存されない) を加えたものが、MockEloquent で表現できる失敗の全体です。
次に読む
- データ取得と DML、IEntry、Mock: 使い方ガイド
- API リファレンス: Scribe: クエリ組み立て側
- API リファレンス: IEntry / Entry / MockEntry: 返り値の
IEntry側 - Layered Constructor Pattern:
IEloquentを Usecase に DI する設計 - ApexEloquent ガイド: ガイド目次に戻る