ApexBlueprint
Write the data's final shape, as itself.Dependency resolution and insert ordering are the framework's job.
Relaying parent Ids, bookkeeping insert order, one more factory method per flag — the procedures that follow you around integration-test data prep collapse into a single declaration.
// The shape of this code is the shape of the dataSOrchestrator 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')));// Zero parent Id relays. Zero insert ordering.Three Contacts and an Opportunity hang under the Account. The indentation is the hierarchy, andwithChildren wires the parent-child links for you.
Four pains, one design
This isn't a list of four features. It's four procedures you run into while preparing integration-test data, closed off structurally.
"Hand the parent's Id to the next insert through a variable."
withChildren anduse turn parent-child links into a declaration, so the bucket brigade of catching an Id after insert and passing it on disappears. Ordering and Id propagation are resolved bySOrchestrator through a topological sort.
"createOppFlagA / FlagB / FlagC. One more method per flag."
Gather the shared fields into template() and override only the delta under test with set(). The march of boolean arguments and the method explosion are gone, and the factory layer stays thin.
"200 lines of data prep. So what does it actually build?"
Nesting with withChildren makes the code's indentation the data's hierarchy, and the code that carries parent-child links around in variables disappears. Read it top to bottom andwhat gets built is right there.
"What insert order works here? Sorting dependencies in your head."
The order of add() is yours to choose; SOrchestrator builds a dependency graph and rearranges it into the correct DML order. Circular dependencies and duplicate aliases are caught at create() and raise an exception.
Test data is a tree.You keep writing it as a list.
Integration-test data is a tree spreading from parents to children, yet a procedural factory flattens it into "a linear sequence of inserts, top to bottom". ApexBlueprint lets you write the tree as a tree: the code's indentation is the hierarchy, withChildren is the branch, and SOrchestrator works out the planting order. Which meansyou no longer translate structure into procedure.
How it reads
The same Account + Contact ×3 + Opportunity: on the left with a procedural TestDataFactory, on the right with ApexBlueprint. The pink lines are the "parent Id wiring" only the procedural side needs; the green lines are the withChildren that erases it.
Procedurally, you catch the inserted parent's Id in a variable and hand it to the child.
Declaratively, withChildren wires the parent-child link, so the relay code is gone.
The indentation on the right is the hierarchy of the data it builds (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();On an early, simple test this is barely different from a one-line factory call. It starts to pay off once the relationships deepen and the references begin to cross. Even a "diamond, not a tree" dependency likeAccount → Opportunity → Quote → QuoteLineItem plus a sharedProduct2 is declared with nothing butwithChildren and use(relations and bulk generation).
Permission tests need hostile data
Permission tests get postponed because the setup is heavy. Creating a single restricted user already lines up small traps: unique Usernames, mixed DML, locale defaults, resolving a Profile by name. And the part that actually matters comes after that — unless you preparethe data that must not be visible to that user, nothing you assert inside System.runAs means anything when it passes.
SPersona collapses it into one chain that names a Profile and its permission sets. Usernames are issued as UUIDs so they never collide under parallel execution, and mixed DML is avoided internally.You don't write a UserFactory.
owner() moves ownership to another user. Records you created yourself are visible to you, soonly once ownership moves can you test "this should not be visible".
sharedWith() declares a manual share as a final state. Assembling the __Share record and inserting it after its parent both disappear behind the declaration.
User admin = SPersona.of('admin').profile('System Administrator').create();User rep = SPersona.of('sales-rep') .profile('Standard User') .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' reads, 'hidden' stays invisible — that is the expectation}Whether you declared a share is itself the expected visibility.If a record you never wrote asharedWith for turns up insiderunAs, that is a hole in your sharing configuration, not a flaw in the test. Reading the declarations above the block tells you exactly what this test claims should be invisible.
A self-contradictory declaration stops before it reaches the DML. Sharing an object whose org-wide default is Public, or sharing with the owner itself, raisesApexBlueprintException during the analysis stage ofcreate()(SPersona API reference).
Declare the shape. Delegate the rest.
The API is 11 methods. Nothing here is bolted on; all of it follows from a single design decision — declare the final state, delegate the resolution (a declarative data specification).
SBlueprintThe blueprint for one record. Fields, bulk generation, parent-child links, ownership and sharing — all in one chain.
SOrchestratorRearranges the registered blueprints with a topological sort and resolves their dependencies.
.create()Runs the real DML in the correct order, and getByAlias retrieves the created records.
The order of add(), the insert order, the parent Id propagation — SOrchestrator takes on every procedural part. You write only what data you want, and afterwardsgetByAlias pulls records out by alias for your assertions, so no SOQL is needed in the test body either.
Deep Dives
Why ApexBlueprint took this shape, across five documents.
Position in Apex Stem
ApexBlueprint is the Test Data Factory among the four OSS libraries that make up Apex Stem. It appears wherever real DML runs in the Handler integration tests of theHandler-Usecase Architecture, taking on the "Handler integration test" side of thetest strategy.
- Handler-Usecase Architecture: the context of the Handler integration tests that call ApexBlueprint
- Test Strategy: integration testing built around
SBlueprint/SOrchestrator - ApexEloquent: the data-access OSS covering the unit-test side of the same strategy
- Apex Stem Introduction Guide: four adoption steps with working code
Start with the Developer Guide
Three steps, followed through in real code: prerequisites and installation, declaring a single record with SBlueprint, then dependency resolution and DML with SOrchestrator.