A difficulty ladder for Salesforce test data
Which objects are cheap to fake, which are expensive, and which you should stop trying to build.
Every Apex test starts with the same question: how much does it cost to make the data this needs exist?
Nobody writes that number down, so it gets rediscovered one painful @TestSetup at a time. Here's the ladder I use now, cheapest first.
Tier one — free
Objects with no required fields beyond a name, no validation rules in your org, no side effects.
Account acc = new Account(Name = 'Ladder Co');
insert acc;
If everything you need lives here, write as many tests as you like.
Tier two — cheap, with reading required
Standard objects your org has customised. Still one insert, but you have to know what was added. Required fields, record types, and validation rules that only fire on some combination of values.
The trap is guessing. Read the rule instead:
sf data query --use-tooling-api \
--query "SELECT ValidationName, Metadata FROM ValidationRule WHERE Id='03d...'" --json
# -> Metadata.errorConditionFormula
Often the cheapest fix is to stop tripping the rule rather than satisfy it. A rule that only fires on active records costs nothing if your test doesn't need an active record.
Tier three — expensive, because the object is a graph
Objects that only become themselves once something else exists. Commerce is full of these. An account isn't a buyer because you set a field; it's a buyer because a BuyerAccount exists:
insert new BuyerAccount(
Name = acc.Name + ' Buyer',
BuyerId = acc.Id,
IsActive = true,
CommerceType = 'Buyer'
);
Then the buyer group, the entitlement policy, the store mapping, the price book, the entry. Six inserts before the first assertion. Build it once in @TestSetup, share it across the class, and never build it twice.
Tier four — stop
Data you cannot create in a test context, or can only create by reimplementing a platform feature: permission-dependent visibility, certain sharing recalculations, anything that depends on org-level async processing.
The answer here isn't a cleverer fixture. It's to move the logic that depends on that data into a method that receives it, and test that. The thin layer that fetches it gets one integration test and an honest comment.
That's the whole ladder. The point of pricing the tiers is that you stop spending tier-three effort on tier-one logic, which is where most @TestSetup files went wrong in the first place.
Next in this series. 4 · The permission set is a dependency takes tier four apart properly, including the error message that lies to you. Every part is collected on the series page.