Apex Test Data Factory
Hello,
Have you ever struggled with the creation of test data for apex unit tests?
Duplicated code was your nightmare? Test Data Builder wasn’t helpful?
Don’t worry. I have a solution.
Problem
Unit Tests – most of the developers think about it as an annoying duty. Salesforce requires at least 75% code coverage.
However, Unit Tests can be a great tool to keep our code error-free.
Written in a good manner, allows us to make code changes without worrying about breaking the whole product.
The most problematic part of test development is test data.
Poor database design, a lot of validation rules and we can stuck in your @testSetup.
And here I present you a solution => Test Data Factory.
Prepare classes responsible for data creation once and invoke them through TDF_TestDataCreator.
*TDF = Test Data Factory
It’s very simple. Let’s go further.
Architecture
Code
The whole TDF’s code you can find here.
// TDF_TestDataCreator.cls
@isTest
public class TDF_TestDataCreator {
private final static Map<sObjectType, System.Type> OBJECT_TO_FACTORY = new Map<sObjectType, System.Type>{
Account.sObjectType => TDF_Accounts.class,
Contact.sObjectType => TDF_Contacts.class
// other factories here
};
public static TDF_SubFactory get(sObjectType objectType) {
return (TDF_SubFactory) getObjectFactory(objectType).getDefaultVariation().newInstance();
}
public static TDF_SubFactory get(sObjectType objectType, String variant) {
return (TDF_SubFactory) getObjectFactory(objectType).get(variant);
}
private static TDF_Factory getObjectFactory(sObjectType objectType) {
return (TDF_Factory) OBJECT_TO_FACTORY.get(objectType).newInstance();
}
}
// TDF_Accounts.cls
@isTest
public class TDF_Accounts extends TDF_Factory {
public override System.Type getDefaultVariation() {
return TDF_VariantAAccount.class;
}
public override Map<String, System.Type> getVariantToSubFactory() {
return new Map<String, System.Type>{
'VARIANT_A' => TDF_VariantAAccount.class,
'VARIANT_B' => TDF_VariantBAccount.class
//other variants here
};
}
}
// TDF_AccountVariantA.cls
@isTest
public with sharing class TDF_VariantAAccount extends TDF_SubFactory {
public override sObject getRecord(Integer index) {
return new Account(
Name = 'Variant A' + index
//other fields here
);
}
}
Usage
Account myTestAccount = (Account) TDF_TestDataCreator.get(Account.sObjectType, 'VARIANT_A').put();
Contact myTestContact = (Contact) TDF_TestDataCreator.get(Contact.sObjectType, 'VARIANT_C').withRelatedRecord(myTestAccount).put();
List<Account> accounts = (List<Account>) TDF_TestDataCreator.get(Account.sObjectType, 'VARIANT_A')
.withFieldValue(Account.Name, 'My New Account Name')
.put(5);
Configuration
All configuration options you can find in TDF_SubFactory.cls. TDF_SubFactory is kind of Builder Design Pattern.
Chain methods to get records valid for your test scenario.
All configuration options
.withFieldValue(sObjectField field, Object value);
List<Account> accounts = (List<Account>) TDF_TestDataCreator.get(Account.sObjectType, 'VARIANT_A')
.withFieldValue(Account.Name, 'My New Account Name')
.put(5);
Result: All Accounts Name will be replaced with My New Account Name, no matter of factory configuration.
withFieldValuePerRecord(List<Map<sObjectField, Object>> customFieldsValues)
List<Account> accounts = (List<Account>) TDF_TestDataCreator.get(Account.sObjectType, 'VARIANT_A')
.withFieldValuePerRecord(new List<Map<sObjectField, Object>>{
new Map<sObjectField, Object>{
Account.Name => 'My New Account Name 1'
},
new Map<sObjectField, Object>{
Account.Name => 'My New Account Name 2'
}
})
.put(2);
Result: Each account will have a different Name.
accounts[0].Name => ‘My New Account Name 1’, accounts[1].Name => ‘My New Account Name 2’
withRelatedRecord(sObject relatedRecord)
Account myTestAccount = (Account) TDF_TestDataCreator.get(Account.sObjectType, 'VARIANT_A').put();
Contact myTestContact = (Contact) TDF_TestDataCreator.get(Contact.sObjectType, 'VARIANT_A').withRelatedRecord(myTestAccount).put();
Result: Contact.AccountId will be taken from Account.Id as it is done in SubFactory Mapping (getMandatoryFields
)
put()
andput(Integer amount)
Account account = (Account) TDF_TestDataCreator.get(Account.sObjectType, 'VARIANT_A').put();
List<Account> accounts = (List<Account>) TDF_TestDataCreator.get(Account.sObjectType, 'VARIANT_A').put(5);
Result: Account will be inserted. 5 Accounts will be inserted.
get()
andget(Integer amount)
Account account = (Account) TDF_TestDataCreator.get(Account.sObjectType, 'VARIANT_A').get();
List<Account> accounts = (List<Account>) TDF_TestDataCreator.get(Account.sObjectType, 'VARIANT_A').get(5);
Result: Get an Account without the insert. Get 5 Accounts without the insert.
Benefits
- No class dependencies.
- Single Responsibility Principle – Each
TDF_Factory
andTDF_SubFactory
are responsible for creating a specific set of data. - Open/Close Principle – Easy to add new factories without changing existing code.
- Easy to understand and use.
- The developer doesn’t need to know concrete classes – invoke
TDF_TestDataCreator
, pass type and variant, and get the record. - One place to fail – You need to fix test data creation only in a specific factory.
- TDF is lightweight – CPU Time and Heap Size are reduced.
Repository
If you have any questions feel free to ask in the comment section below. 🙂
Was it helpful? Check out our other great posts here.