The Test Data Factory Pattern That Saves Hours Every Sprint
Most Salesforce projects start with test classes that create their own data inline. By sprint three, you have 40 test classes each creating Accounts, Contacts, and Opportunities with slightly different field values. When a validation rule changes, you update test data in 40 places. When a required field is added, 40 test classes break. This is the #1 source of deployment failures I see.
The fix is a TestDataFactory class. One class, one place to create every test record. Every test method calls TestDataFactory.createAccount() instead of inserting its own. When a validation rule changes, you update one method. When a required field is added, you add it in one place. The factory methods should accept optional parameter overrides so test methods can customize only the fields they care about.
Make the factory methods return inserted records (not just instantiated) so test methods do not need to worry about insert order or required lookups. Use @TestVisible on the factory class and SeeAllData=false on every test class. This is the single highest-ROI pattern in Salesforce testing.
Code Example
@IsTest
public class TestDataFactory {
public static Account createAccount() {
return createAccount(new Map<String, Object>());
}
public static Account createAccount(Map<String, Object> overrides) {
Account acc = new Account(
Name = 'Test Account ' + generateUniqueName(),
BillingState = 'FL',
Industry = 'Technology'
);
// Apply any field overrides
for (String field : overrides.keySet()) {
acc.put(field, overrides.get(field));
}
insert acc;
return acc;
}
public static Contact createContact(Id accountId) {
return createContact(accountId, new Map<String, Object>());
}
public static Contact createContact(
Id accountId, Map<String, Object> overrides
) {
Contact con = new Contact(
AccountId = accountId,
FirstName = 'Test',
LastName = 'Contact ' + generateUniqueName(),
Email = generateUniqueName() + '@test.example.com'
);
for (String field : overrides.keySet()) {
con.put(field, overrides.get(field));
}
insert con;
return con;
}
public static Opportunity createOpportunity(Id accountId) {
Opportunity opp = new Opportunity(
AccountId = accountId,
Name = 'Test Opp ' + generateUniqueName(),
StageName = 'Prospecting',
CloseDate = Date.today().addDays(30),
Amount = 10000
);
insert opp;
return opp;
}
private static String generateUniqueName() {
return String.valueOf(Datetime.now().getTime())
+ String.valueOf(Math.random()).substring(2, 6);
}
}Need this implemented in your org?
I've shipped these patterns in production for 10+ years.
View Consulting →Enjoyed this? Get more like it.
Glen's Musings — AI, investing, and building things. Occasional. Free.
More Apex Tips
Apex Bulkification: The Only Pattern You Need to Memorize
Every Apex developer learns "don't put SOQL in a loop" on day one. But bulkification goes deeper than that. The real pat...
Read moreAdvancedQueueable vs Future vs Batch: When to Use Each Async Pattern
Salesforce gives you three async patterns and choosing the wrong one wastes hours. Here is the decision tree I use on ev...
Read moreIntermediateCustom Metadata Types: Runtime Configuration Without Deployments
Custom Settings were the old way to store configuration. Custom Metadata Types are the upgrade, and the difference matte...
Read more