Apex Stem · Test Data Factory
v2.0.1·リリースノート

ApexBlueprint

そのデータの "最終形" を、そのまま書く。依存の解決も、insert の順序も、フレームワークが引き受ける。

親 Id の持ち回り、insert 順の管理、フラグごとに増えるファクトリメソッドといった、 結合テストのデータ準備につきまとう手続きを、宣言ひとつに畳みます。

OrderScenario_T.cls — integration test
// このコードの "形" が、そのままデータの "形"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 つの手続きを、構造ごと閉じます。

The pain

「親の Id を、変数で次の insert へ手渡していく」

What changes

withChildrenuse が親子リンクを「宣言」に変えるので、 insert 後の Id を変数で受けて次へ渡す "バケツリレー" は消え、 順序と Id 転記は SOrchestrator が トポロジカルソートで解決してくれます。

SOrchestrator の依存解決を読む →
The pain

「createOppFlagA / FlagB / FlagC。フラグの数だけメソッドが増える」

What changes

共通項目は template() に集約し、 そのテストで検証する差分だけを set() で上書きすれば、 boolean 引数の行進とメソッド爆発は消え、ファクトリ層は薄いまま保たれます。

template + set の分業を読む →
The pain

「200 行のデータ準備。で、結局どんな構造ができるの?」

What changes

withChildren のネストで コードのインデント階層がそのままデータ階層になり、親子関係を変数で持ち回るコードも消えるので、 上から読むだけで 「最終的に何が作られるか」が一目で分かります。

withChildren の階層表現を読む →
The pain

「どの順に insert すれば通る? 依存を頭の中で並べ替える」

What changes

add() の順序は自由で、 SOrchestrator が依存グラフを作って正しい DML 順序へ並べ替えます。 循環依存や alias 重複があれば create() の時点で検知され、例外になります。

依存解決の内部を読む →
The shape you keep flattening

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 です。

Manual relay

手続き型では、insert した親の Id を 変数で受けて子へ手渡します。

Auto-wired

宣言型では withChildren が 親子リンクを自動配線するので、持ち回りコードは消えます。

The shape

右のインデント階層が、そのまま生成されるデータの階層 (Account > Contact / Opportunity) になります。

TestDataFactory.clsProcedural
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;
OrderScenario_T.clsDeclarative
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 のような "木ではないダイヤ型" の依存でも、withChildrenuse だけで宣言できます (関係と量産の応用)。

Permission tests need hostile data

権限のテストが後回しになるのは、準備が重いからです。制限ユーザーを 1 人作るだけで、 Username の一意性・mixed DML・ロケール既定値・Profile 名の解決といった細かい罠が並びます。 そして本当に効くのはその先で、「そのユーザーから見えてはいけないデータ」を用意しない限り、System.runAs の中で何をアサートしても、 通ったことに意味がありません。

Persona

SPersona が Profile と権限セットを指定するだけの 1 チェーンにまとめます。 Username は UUID で払い出されるので並列実行でも衝突せず、mixed DML も内部で回避します。UserFactory を自前で書く必要はありません。

Ownership

owner() で所有者を別のユーザーに寄せます。 自分が作ったレコードは自分に見えてしまうので、所有者を動かして初めて「見えないはず」を試せる状態になります。

Sharing

sharedWith() は手動共有を最終状態として宣言します。__Share レコードの組み立ても、 親より後に insert する順序も、宣言の裏側に隠れます。

InvoiceVisibility_T.clsDeclarative
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 メソッドだけ。どの機能も後付けではなく、 すべては「最終状態を宣言し、解決を委譲する」という ひとつの設計判断 (宣言的データ仕様) から導かれています。

Declaration
SBlueprint

1 レコードの設計図。項目も、量産も、親子も、所有者と共有も ひとつのチェーンで。

Resolution
SOrchestrator

追加した設計図をトポロジカルソートで並べ替え、依存を解決します。

Execution
.create()

正しい順序で実 DML を流し、getByAlias で生成済みレコードを取り出せます。

add() の順序、 insert の順序、親 Id の転記も、手続きの部分はすべて SOrchestrator が引き受けます。 あなたは「どんなデータが欲しいか」だけを書けばよく、生成後はgetByAlias で alias からレコードを取り出してアサーションできるので、テスト本体に SOQL を書く必要もありません。

宣言的データ仕様の思想を深く読む

Position in Apex Stem

ApexBlueprint は Apex Stem を構成する 4 つの OSS のうち Test Data Factory を担います。Handler-Usecase Architecture の Handler 結合テストで実 DML が走る場面に登場し、テスト戦略 の 「Handler 結合テスト」側を引き受けます。

ApexEloquent
Data Access: SOQL / DML + モック
ApexBlueprint
Test Data Factory: 実 DML の結合テストデータ
ApexTrace
Lifecycle Logging: Usecase の経路追跡とテスト検証
ApexTools
Foundation: TriggerHandler 基底 + HTTP DI ラッパー
Related Documents

Start with the Developer Guide

前提とインストール、SBlueprint での単一レコード宣言、SOrchestrator での依存解決と DML という 3 つのステップを、実コードで辿ります。