ApexBlueprint
そのデータの "最終形" を、そのまま書く。依存の解決も、insert の順序も、フレームワークが引き受ける。
親 Id の持ち回り、insert 順の管理、フラグごとに増えるファクトリメソッドといった、 結合テストのデータ準備につきまとう手続きを、宣言ひとつに畳みます。
// このコードの "形" が、そのままデータの "形"SOrchestrator orchestrator = SOrchestrator.start() .add( SBlueprint.of(Account.class) .template(Blueprints.accBasic()) .alias('acc') .withChildren( SBlueprint.of(Contact.class) .set('LastName', 'Yamada-{#}') .times(3)) .withChildren( SBlueprint.of(Opportunity.class) .set('StageName', 'Prospecting')));// 親 Id の持ち回りゼロ。insert 順の指定もゼロ。Account の下に Contact ×3 と Opportunity がぶら下がり、インデントがそのまま階層に、 親子リンクは withChildren が自動で配線します。
Four pains, one design
機能を 4 つ並べたリストではありません。結合テストのデータ準備で誰もが踏む 4 つの手続きを、構造ごと閉じます。
「親の Id を、変数で次の insert へ手渡していく」
withChildren とuse が親子リンクを「宣言」に変えるので、 insert 後の Id を変数で受けて次へ渡す "バケツリレー" は消え、 順序と Id 転記は SOrchestrator が トポロジカルソートで解決してくれます。
「createOppFlagA / FlagB / FlagC。フラグの数だけメソッドが増える」
共通項目は template() に集約し、 そのテストで検証する差分だけを set() で上書きすれば、 boolean 引数の行進とメソッド爆発は消え、ファクトリ層は薄いまま保たれます。
「200 行のデータ準備。で、結局どんな構造ができるの?」
withChildren のネストで コードのインデント階層がそのままデータ階層になり、親子関係を変数で持ち回るコードも消えるので、 上から読むだけで 「最終的に何が作られるか」が一目で分かります。
「どの順に insert すれば通る? 依存を頭の中で並べ替える」
add() の順序は自由で、 SOrchestrator が依存グラフを作って正しい DML 順序へ並べ替えます。 循環依存や alias 重複があれば create() の時点で検知され、例外になります。
Test data is a tree.You keep writing it as a list.
結合テストのデータは親から子へ広がる木構造なのに、手続き型のファクトリは それを「上から順に insert する直列の手順」に潰してしまいます。ApexBlueprint は木を木のまま書かせ、 コードのインデントが階層を、withChildren が枝を表し、 SOrchestrator が「どの順に植えるか」を計算します。だから構造を、手順へ翻訳しなくてよくなりました。
How it reads
同じ Account + Contact ×3 + Opportunity を、左は手続き型の TestDataFactory で、右は ApexBlueprint で作ります。 ピンクの行が手続き型にだけ必要な「親 Id の配線」、緑の行がそれを消し去るwithChildren です。
手続き型では、insert した親の Id を 変数で受けて子へ手渡します。
宣言型では withChildren が 親子リンクを自動配線するので、持ち回りコードは消えます。
右のインデント階層が、そのまま生成されるデータの階層 (Account > Contact / Opportunity) になります。
Account a = new Account(Name = 'Acme');insert a; List<Contact> cons = new List<Contact>();for (Integer i = 1; i <= 3; i++) { cons.add(new Contact( LastName = 'Yamada-' + i, AccountId = a.Id));}insert cons; Opportunity o = new Opportunity( Name = 'Renewal', StageName = 'Prospecting', AccountId = a.Id);insert o;SOrchestrator orchestrator = SOrchestrator.start() .add( SBlueprint.of(Account.class) .set('Name', 'Acme') .alias('acc') .withChildren( SBlueprint.of(Contact.class) .set('LastName', 'Yamada-{#}') .times(3)) .withChildren( SBlueprint.of(Opportunity.class) .set('Name', 'Renewal') .set('StageName', 'Prospecting'))); orchestrator.create();初期の単純なテストでは factory の 1 行と大差ありませんが、 関係が深くなり参照が交差し始めると効いてきます。Account → Opportunity → Quote → QuoteLineItem + 共有 Product2 のような "木ではないダイヤ型" の依存でも、withChildren と use だけで宣言できます (関係と量産の応用)。
Permission tests need hostile data
権限のテストが後回しになるのは、準備が重いからです。制限ユーザーを 1 人作るだけで、 Username の一意性・mixed DML・ロケール既定値・Profile 名の解決といった細かい罠が並びます。 そして本当に効くのはその先で、「そのユーザーから見えてはいけないデータ」を用意しない限り、System.runAs の中で何をアサートしても、 通ったことに意味がありません。
SPersona が Profile と権限セットを指定するだけの 1 チェーンにまとめます。 Username は UUID で払い出されるので並列実行でも衝突せず、mixed DML も内部で回避します。UserFactory を自前で書く必要はありません。
owner() で所有者を別のユーザーに寄せます。 自分が作ったレコードは自分に見えてしまうので、所有者を動かして初めて「見えないはず」を試せる状態になります。
sharedWith() は手動共有を最終状態として宣言します。__Share レコードの組み立ても、 親より後に insert する順序も、宣言の裏側に隠れます。
User admin = SPersona.of('admin').profile('システム管理者').create();User rep = SPersona.of('sales-rep') .profile('標準ユーザー') .permissionSets('InvoiceReadOnly') .create(); SOrchestrator orchestrator = SOrchestrator.start() .add( SBlueprint.of(Invoice__c.class).alias('shared') .owner(admin) .sharedWith(rep, 'Read')) .add( SBlueprint.of(Invoice__c.class).alias('hidden') .owner(admin));orchestrator.create(); System.runAs(rep) { // 'shared' は読めて、'hidden' は見えない — が期待値}共有を宣言したかどうかが、そのまま可視性の期待値になります。sharedWith を書いていないレコードがrunAs の中で見えてしまったら、 それはテストの不備ではなく共有設定の穴です。ブロックの外側の宣言を読むだけで、 そのテストが何を「見えないはず」と主張しているのかが分かります。
宣言そのものが矛盾していれば、DML に到達する前に止まります。 組織の共有設定が Public のオブジェクトへの共有宣言も、オーナー自身への共有も、create() の解析段階でApexBlueprintException になります (SPersona の API リファレンス)。
Declare the shape. Delegate the rest.
API は 11 メソッドだけ。どの機能も後付けではなく、 すべては「最終状態を宣言し、解決を委譲する」という ひとつの設計判断 (宣言的データ仕様) から導かれています。
SBlueprint1 レコードの設計図。項目も、量産も、親子も、所有者と共有も ひとつのチェーンで。
SOrchestrator追加した設計図をトポロジカルソートで並べ替え、依存を解決します。
.create()正しい順序で実 DML を流し、getByAlias で生成済みレコードを取り出せます。
add() の順序、 insert の順序、親 Id の転記も、手続きの部分はすべて SOrchestrator が引き受けます。 あなたは「どんなデータが欲しいか」だけを書けばよく、生成後はgetByAlias で alias からレコードを取り出してアサーションできるので、テスト本体に SOQL を書く必要もありません。
Deep Dives
なぜ ApexBlueprint がこの形になったのか。5 本のドキュメントで深掘りします。
Position in Apex Stem
ApexBlueprint は Apex Stem を構成する 4 つの OSS のうち Test Data Factory を担います。Handler-Usecase Architecture の Handler 結合テストで実 DML が走る場面に登場し、テスト戦略 の 「Handler 結合テスト」側を引き受けます。
- Handler-Usecase Architecture: ApexBlueprint が呼ばれる Handler 結合テストの文脈
- Test Strategy:
SBlueprint/SOrchestratorを軸にした結合テスト - ApexEloquent: 同じ戦略の単体テスト側を担うデータアクセス OSS
- Apex Stem Introduction Guide: 動くコード付きの導入 4 ステップ
Start with the Developer Guide
前提とインストール、SBlueprint での単一レコード宣言、SOrchestrator での依存解決と DML という 3 つのステップを、実コードで辿ります。