If your Salesforce org is anything like ours, its complex. To prevent regression, complex and meaningful tests are required. In our situation, E2E tests depend on a lot of configuration records, before they can perform their operations. Today I focus on how to streamline the test data creation in Apex E2E tests. I will introduce the SObjectTreeLoader, a utility that enables you to set up a complex structure of interrelated records. This is about test data setup for E2E tests, not about unit testing, mocking and bulk testing.

Think about a scenario that requires you to set up a business account with multiple contacts. You want a service contract on that account which uses one of the contacts as a billing contact. Now add products with multi-currency prices to the mix. The products need to be specially configured subscription products with minimum contract terms, renewal terms, and advanced billing and licensing configuration. The actual configuration values matter, because it drives the behavior of your application. You cannot auto-generate records based on a simple rule set. You need to prepare a highly specific set of interrelated records before you can start testing. This is the problem the SObjectTreeLoader addresses.

Who Is This Relevant For?

I will assume that you are familiar with the @testSetup annotation and that you know about good testing strategies. If you still create your test data in static private class initializers (trust me, I’ve seen things …) or if your test classes contain a single method that looks like this, this post may not be relevant for you.

public static testMethod void testFeedback(){
    Test.startTest();
    Account acc = new Account(Name='Test');
    insert acc;
    Contact con = new Contact(LastName='Test');
    insert con;
    Entitlement ent1 = new Entitlement(Name='Base',AccountId=acc.Id);
    insert ent1;
    Entitlement ent2 = new Entitlement(Name='Pilot',AccountId=acc.Id);
    insert ent2;
    Case c = new Case(Priority='No SLA');
    insert c;
    Order ord = new Order(EffectiveDate=Date.Today(),accountid=acc.id,status='Draft',Pricebook2Id=test.getStandardPricebookId());
    insert ord;
    feedPageControllerNEW.addFeedback(con.Id,c.Id,'10','Test');
    feedPageControllerNEW.addFeedback(con.Id,ord.Id,'10','Test');
    feedPageControllerNEW.checkFeedbackOnObject(c.Id);
    feedPageControllerNEW.checkFeedbackOnObject(ord.Id);
    Test.stopTest();
}

On the other hand, if your test setup requires the creation of 100s of records across 10s of different SObjects, this post is for you. Take this test setup code in our Fulfillment package:

  • It creates Client Profiles with Tax rules (this is our solution for separating business units on a single org),
  • Sets up Product, a Pricebook, and Prices in a multi-currency environment,
  • Creates Accounts with Contacts,
  • And finally adds Orders, Service Contracts and Quotes (with line items).

All this is needed, so our tests can verify that orders can be created from opportunities, orders can be integrated into existing contracts, and new contracts can be created from orders. Since we are in an „end-to-end“ context, we cannot stub these dependencies. Tests must perform DML and their asserts act mostly on queried records.

@testSetup
static void makeData() {
    ClientProfile__c testProfile = FulfillmentFixtures.InsertDefaultClientProfiles()[0];
    Pricebook2 testPricebook = FulfillmentFixtures.Products.insertMinimalProductsWithCustomPrices(PRICEBOOK_NAME);
    Account customer = FulfillmentFixtures.Sales.insertFullOrderStructure(testProfile.Id, testPricebook.Id);
    ServiceContract existingContract = FulfillmentFixtures.Billing.insertServiceContract(customer.Id, null);
    Opportunity opp = FulfillmentFixtures.Sales.insertOpportunityWithQuote(customer.Id, testPricebook.Id);
}

The actual test data creation is already well encapsulated in fixtures. But these fixtures execute more than 400 lines of code in total. This approach worked well for a couple years, but eventually we ran into a huge problem.

What’s The Problem with Fixtures?

Fixture classes become too complex to maintain. We soon ended up with 800+ lines for a single fixture class. Per package. We then tried to separate concerns by creating sub-classes to isolate SalesFixtures from BillingFixtures, but this only treated the symptoms, not the underlying root cause. More classes with fewer lines don’t solve the problem of too specific duplicated code and too complex class APIs. Our code is not generalized enough: We need to achieve the same functionality with fewer lines of code. We need a solution that is less complex and has more flexibility.

Achieving Flexibility in Test Data Creation

The problem is this: Test data creation needs to be flexible to allow you to test edge cases. You want to standardize certain elements (like a set of business accounts with contacts), but need parameterization for others (opportunities in different stages). Add custom logic if you have a product lifecycle management that limits when new products can be added to a quote, but the quote can still be closed & converted to an order. All this forces you to create a lot of necessary complexity, if you want to re-use at least some code, and still need 10 different ways how products are created.

And Your Fixture Classes Get Out of Hand

It doesn’t take long until you end up with a class that looks similar to this one (the screenshot only shows lines 218 to 360 out of 800).

A screenshot of billing fixtures  apex subclass. The class has multiple overloaded methods for test data creation.

Or here is an example of product fixtures from our PIM (product information management) package:

Test methods to initialize product test data from our PIM fixtures.

There is no one to blame here. This is just the way it happens, if you don’t have a framework to streamline your test data creation. The Test.load APIs from Salesforce are not useful as well. The result? One of this happens:

  1. Developers copy/paste existing test setup.
  2. They create their own fixture, adding to the complexity.

Because both options are simpler than trying to understand (or even modify!) existing ones. To make things worse, we need to be very strict on how we can share fixtures across package domains. We don’t want to introduce unnecessary coupling via test-setup-dependency. This results in a lot of redundancy, because all packages need some sort of account and contact setup. Just slightly different. No chance to reuse, so we duplicate.

How Can We Streamline Test Data Creation?

There is no silver bullet that solves all my problems (who would have guessed?). But I found a solution, that addresses our core problems:

  • Complex variations (15 products, each slightly different) require a lot of overloaded methods with hard-coded values. It is hard to get a grasp of the actual records that are being inserted.
  • It is impossible to inject small variations without adding to the overall complexity to the fixtures.
  • A lot of complexity is needed when inserting large sobject trees of interrelated records (such as an Account with Contacts and an Order with these Contacts as billing and shipping contacts set, etc.)

Most if this is essentially caused by one design principle: The records we insert, are initialized in apex classes.

A SObject Tree Importer for Test Data Creation

I always liked the SObject Tree API and its corresponding sf data import tree command. I believe, a similar experience can be achieved for test data setup in our Apex E2E tests.

The general idea is simple: You define your specific records templates in a JSON file (e.g. your Account records). These record files can be combined to a plan file, that defines the order of insertion and how relationships are resolved.

Optionally, you can instruct the plan to bypass all triggers (at least if you implemented my progressive Apex Trigger Framework. This is extremely helpful if you want to write tests for data migrations, cover edge cases with corrupted data, or simply struggle with performance.

{
  "sobjectName": "Account",
  "resolveRefs": true,
  "bypassTriggers": true,
  "files": ["Apex_Utils_Default_Business_Accounts"]
}

The records files are almost identical to what the sf data import tree command processes, with a little bit of extras.

A single file that templates contact records.
Apex_Utils_Default_Contacts.json

Note the AccountId that references an Account (@Starship), that is defined in the Apex_Utils_Default_Business_Accounts.json.

Now to the extras I mentioned.

Resolving Record Type Names Dynamically

One big limitation of the standard sf data tree commands is the ability to resolve record types correctly. With the SObjectTreeLoader, you can reference a record type by its DeveloperName.

{
    "attributes": {
      "type": "Campaign",
      "referenceId": "CampaignRef1"
    },
    ...
    "RecordTypeId": "@My_Campaign_Record_Type"
}

Dynamic Date Formulas

The library also supports a SOQL-like syntax to express relative dates: To dynamically resolve to a date value that is 30 days in the future, use TODAY:ADD_DAYS:30. It currently supports a limited set of functions (see official documentation for more details).

Putting it Together: Loading Test Data in Apex Test

Loading your entire structure of test data can be as simple as executing a single command in test setup. Alternatively, you can also construct your own SObjectTreeLoader instance from a plan file or manually and „program“ it.

@testSetup
static void makeData() {
  TestData.load('Apex_Utils_Demo_Plan');
}

We found that this works particularly well for complex sets of interrelated records that contain a mix of configuration records (products, product rules, license configurations) and test records (accounts, contacts, orders and contracts).

This makes it very easy to reuse record template files from upstream dependencies, while still keeping them loosely coupled. If you require slightly different account setup, your downstream dependency can override the account template file, but still reuse the other objects.

Limitations of Plan Import Syntax

Like every library, this one also has its limitation. It is important to understand where and why it shouldn’t be used.

Bulk Record Insert

There are better libraries for inserting large amounts of data, especially if record values are initialized based on simple rules and incrementing numbers. I analyzed some of them in this post. The SObject tree loader would require you to maintain a JSON file that „hard codes“ 200 account records, as opposed to iterating a for(Integer i = 0; i < 200; i++)-loop.

Dependent and Dynamic Test Data Insertion

It is also not feasible for inserting line items to orders, quotes, service contracts. Apex code iterates over all PriceBookEntries and creates *LineItems for each entry. Simple, effective, 5 lines of code. This library would require you to maintain record files for each line item type (e.g. OrderItem) and meticulously link the parent entity (e.g. Order) with the Pricebook, and each line item file with the corresponding PriceBookEntry.

Thoughts for Further Improvement

Not everyone likes storing JSON record templates in static resources. You still end up with a lot of duplication, just not apex code. Better isolated, yet still duplication. So why not delegate record instantiation to an apex class, instead of a JSON file?

I assume it will be a cakewalk to extend the plan interface so it not only processes files, but also factories. A simple interface that any test mock factory can implement to supply dynamic record templates.

Closing Thoughts

I will publish a demo implementation and a more detailed documentation of the concepts in my js-salesforce-apex-utils repository shortly. The idea is to reuse familiar concepts of the sf data tree import command and transfer it to an Apex API that can be used by developers to streamline their test data setup. It is a niche solution for complex setup of interrelated records, and is more expressive and declarative than a traditional pure apex approach.

I have been able to streamline test data setup for a substantial amount of our Apex E2E tests. As it turns out, you rarely need 1000 records based on simple rules for functional testing. But you often need complex, interrelated data that needs to be easy to understand and expressive.