Piotr Gajek - Beyond The Cloud
written byPiotr Gajek
posted on August 21, 2022
Technical Architect and Full-stack Salesforce Developer. He started his adventure with Salesforce in 2017. Clean code lover and thoughtful solutions enthusiast.

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

test data factory architecture

apex test data factory

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() and put(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() and get(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 and TDF_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

Github


If you have any questions feel free to ask in the comment section below. 🙂

Was it helpful? Check out our other great posts here.


Buy Me A Coffee