SObject Test Data Design Pattern

The SObject Test Data Design Pattern consists of a base SObjectTestData apex class that builds SObject records and optionally inserts them. It uses a fluent API to allow method chaining so that the code is self-documenting within test classes. For each SObject used in tests, a corresponding {Object}TestData class is created that extends from SObjectTestData. In each test class, the test methods then use the {Object}TestData classes as needed.
Let’s see an Account example.

Account Test Class

​@isTest
public class AccountTest {
public static testMethod void unitTestHere() {
Account a = AccountTestData.Instance.insertAccount();
Account b = AccountTestData.Instance.withName(‘John Wayne’)
.insertAccount();
}
}

AccountTestData Class

/**
* @description Builder class for dealing with Account records.
* Solely used for testing, NOT a data factory.
**/
@isTest
public class AccountTestData extends SObjectTestData {
/**
* @description Overridden method to set up the default
* Account state for AccountTestData.
* @return A map of Account default fields.
*/
protected override Map<Schema.SObjectField, Object> getDefaultValueMap() {
return new Map<Schema.SObjectField, Object>{
Account.Name => ‘Luke Freeland’
};
}
/**
* @description Returns the SObject type for AccountTestData builder.
* @return Account.SObjectType.
*/
protected override Schema.SObjectType getSObjectType() {
return Account.SObjectType;
}
/**
* @description Sets the name on the account.
* @param name The name that the account will have.
* @return The instance of AccountTestData.
*/
public AccountTestData withName(String name) {
return withDynamicData(Account.Name, name);
}
/* Create a “with” method for each property that can be set */
/**
* @description Builds the Account object.
* @return The created Account object.
*/
public Account create() {
return (Account)super.buildWithReset();
}
/**
* @description Inserts the built Account object.
* @return The inserted Account object.
*/
public Account insertAccount() {
return (Account)super.insertRecord();
}
/**
* @description Gets an instance of AccountTestData.
* @return AccountTestData instance.
*/
public static AccountTestData Instance {
get {
if (Instance == null) {
Instance = new AccountTestData();
}
return Instance;
}
}
/**
* @description Private constructor for singleton.
*/
private AccountTestData() {
super();
}
}

SObject Test Data Class

/**
* @description A fluent interface for creating and inserting SObject records.
* Solely used for testing, NOT a data factory.
*/
public abstract class SObjectTestData {
private Map<Schema.SObjectField, Object> customValueMap;
private Map<Schema.SObjectField, Object> defaultValueMap;
/**
* @description Subclasses of SObjectTestData should call super()
* from within constructors to invoke the setup() method.
*/
public SObjectTestData() {
customValueMap = new Map<Schema.SObjectField, Object>();
defaultValueMap = getDefaultValueMap();
}
/**
* @description Retrieves the value set for the specified field.
* @param field The Schema.SObjectField whos value we are retrieving.
* @return An Object used when constructing SObjects for the specified field.
*/
protected Object currentValueFor(Schema.SObjectField field) {
Object val = customValueMap.get(field);
if (val == null) {
return defaultValueMap.get(field);
}
return val;
}
/**
* @description Generates a map of default values for this SObjectType.
* @return The Map of SObjectFields to their corresponding default values.
*/
protected abstract Map<Schema.SObjectField, Object> getDefaultValueMap();
/**
* @description Dynamically sets the Schema.SObjectField noted by field to value for
* SObjects being built.
* @param field The Schema.SObjectField to map the value to and cannot be null.
* @param value The value for the field and can be set to null.
* @return The instance of SObjectTestData.
*/
protected SObjectTestData withDynamicData(Schema.SObjectField field, Object value) {
customValueMap.put(field, value);
return this;
}
/**
* @description Builds an instance of SObject dynamically and sets the instance’s
* fields from the values in the customValueMap and defaultValueMap.
* @return an instance of SObject
*/
private SObject build() {
beforeBuild();
SObject instance = getSObjectType().newSObject(null, true);
Set<Schema.SObjectField> defaultFields = defaultValueMap.keySet().clone();
defaultFields.removeAll(customValueMap.keySet());
for (Schema.SObjectField field : defaultFields) {
instance.put(field, defaultValueMap.get(field));
}
for (Schema.SObjectField field : customValueMap.keySet()) {
instance.put(field, customValueMap.get(field));
}
afterBuild(instance);
return instance;
}
/**
* @description Builds an instance of SObject dynamically and sets the instance’s
* fields from the values in the defaultValueMap.
* @return an instance of SObject
*/
protected SObject buildDefault() {
beforeBuild();
SObject instance = getSObjectType().newSObject(null, true);
for (Schema.SObjectField field : defaultValueMap.keySet()) {
instance.put(field, defaultValueMap.get(field));
}
afterBuild(instance);
return instance;
}
/**
* @description Builds an instance of SObject dynamically and sets the instance’s
* fields from the values in the customValueMap. Also clears
* the customValueMap.
* @return an instance of SObject
*/
protected SObject buildWithReset() {
SObject instance = build();
customValueMap = new Map<Schema.SObjectField, Object>();
return instance;
}
/**
* @description Builds an instance of SObject dynamically and sets the instance’s
* fields from the values in the customValueMap map. This method does not
* clear the customValueMap.
* @return an instance of SObject
*/
protected SObject buildWithoutReset() {
return build();
}
/**
* @description Inserts the built SObject.
* @return The inserted SObject.
*/
protected SObject insertRecord() {
SObject instance = buildWithReset();
beforeInsert(instance);
insert instance;
afterInsert(instance);
return instance;
}
/**
* @description Inserts the SObject built from only the defaults.
* @return The inserted SObject.
*/
protected SObject insertDefaultRecord() {
SObject instance = buildDefault();
beforeInsert(instance);
insert instance;
afterInsert(instance);
return instance;
}
/**
* @description Inserts a list of SObjects and ties into the before and after hooks.
* @param numToInsert the number of SObjects to insert.
* @return The inserted SObjects.
*/
protected List<SObject> insertRecords(Integer numToInsert) {
List<SObject> sobjectsToInsert = new List<SObject>();
for (Integer i = 0; i < numToInsert; i++) {
SObject sObj = buildWithoutReset();
sobjectsToInsert.add(sObj);
beforeInsert(sObj);
}
insert sobjectsToInsert;
for (SObject sObj : sobjectsToInsert) {
afterInsert(sObj);
}
return sobjectsToInsert;
}
/**
* @description This method allows subclasses to invoke any action before
* the SObject is built.
*/
protected virtual void beforeBuild() {}
/**
* @description This method allows subclasses to handle the SObject after
* it is built.
* @param sObj The SObject that has been built.
*/
protected virtual void afterBuild(SObject sObj) {}
/**
* @description This method allows subclasses to handle the SObject before
* it is inserted.
* @param sObj The SObject that is going to be inserted.
*/
protected virtual void beforeInsert(SObject sObj) {}
/**
* @description This method allows subclasses to handle the SObject after
* it is inserted.
* @param sObj The SObject that has been inserted.
*/
protected virtual void afterInsert(SObject sObj) {}
/**
* @description Returns the SObject type for this TestData builder.
* @return A Schema.SObjectType.
*/
protected abstract Schema.SObjectType getSObjectType();
}