---
title: "Test data factories that survive a schema change"
url: "https://trailassembly.com/apex-test-data-factories/"
topic: "Apex and Development"
published: "2026-09-12T04:03:57+00:00"
updated: "2026-10-06T13:53:17+00:00"
---

# Test data factories that survive a schema change

**In one sentence:** put record construction in one class so that when someone makes a field required next year, one file changes instead of forty test classes. That is the entire argument, and it is enough. The design question is only whether the factory returns records or inserts them, and the answer is returns.

## Return, do not insert

```apex
@IsTest
public class TestFactory {

    public static Account account() {
        return new Account(
            Name     = 'Test Account ' + uniqueSuffix(),
            Industry = 'Technology'
        );
    }

    public static List<Opportunity> opportunities(Id accountId, Integer count) {
        List<Opportunity> out = new List<Opportunity>();
        for (Integer i = 0; i < count; i++) {
            out.add(new Opportunity(
                Name      = 'Opp ' + i,
                AccountId = accountId,
                StageName = 'Prospecting',
                CloseDate = Date.today().addDays(30)
            ));
        }
        return out;
    }
}
```

A factory that inserts forces a DML statement per call, so a test needing three object types spends three of its 150. Returning lets the caller insert once, and it lets a test modify a field before saving, which is most of what tests need to do.

## Let the caller override

The failure mode of a factory is a method per variation: `accountWithHighRevenue`, `accountInRetail`, `accountWithNoOwner`. Twenty methods later nobody can find the one they want. Return the default and let the test change what it cares about:

```apex
Account a = TestFactory.account();
a.AnnualRevenue = 5000000;
insert a;
```

This reads better too. The test says which field matters to it, in the test, where someone debugging will look.

## Uniqueness

Unique fields collide when a test class creates several records and when tests run in parallel. Generate a suffix rather than counting:

```apex
private static Integer counter = 0;
private static String uniqueSuffix() {
    counter++;
    return String.valueOf(Datetime.now().getTime()) + '-' + counter;
}
```

For an email, `'test' + uniqueSuffix() + '@example.invalid'`. The `.invalid` top-level domain is reserved and can never route anywhere, which is a better habit than `example.com` in test data that might one day reach a real send.

## Users belong in a separate method

Inserting a User in the same transaction as an Account throws [MIXED_DML_OPERATION](https://trailassembly.com/mixed-dml-operation/). Keep user creation in its own method and call it inside `System.runAs`, and mark the method clearly so nobody folds it into the general setup.

```apex
public static User standardUser(String profileName) {
    Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
    return new User(
        ProfileId = p.Id,
        LastName  = 'Test',
        Email     = 'test' + uniqueSuffix() + '@example.invalid',
        Username  = 'test' + uniqueSuffix() + '@example.invalid',
        Alias     = 'tst',
        TimeZoneSidKey = 'GMT',
        LocaleSidKey = 'en_GB',
        EmailEncodingKey = 'UTF-8',
        LanguageLocaleKey = 'en_US'
    );
}
```

Setting `TimeZoneSidKey` to GMT deliberately is worth doing: a test that passes only because the [running user](https://trailassembly.com/dashboard-running-user/) is in a particular zone is a test that will fail for someone else.

## Surviving a schema change

The whole point is that a new required field means editing one method. To keep that true:

- **No literal `new Account(...)` in test classes.** Once one exists, the guarantee is gone.

- **No queries for org data** inside the factory, except where unavoidable such as Profile and RecordType. Those need a filter that will still match after someone renames things.

- **No assumptions about validation rules.** A factory record must satisfy the current rules, which means the factory is the place that breaks when a rule is added — deliberately, once.

## Do you need a builder

Fluent builders, `TestFactory.account().withRevenue(5000000).build()`, read nicely and cost a class per object. On a large codebase with many required fields and deep hierarchies they earn it. On a typical org, a static method returning a record plus direct field assignment is less code and no harder to read.

## Frequently asked

**Should the factory class be `@IsTest`?** Yes, so it does not count against org coverage. It also means production code cannot call it, which is correct.

**How do I handle [record types](https://trailassembly.com/record-types/)?** A method taking a developer name and looking it up through describe, not a hard-coded Id. Ids differ between orgs.

**Can I load from a [static resource](https://trailassembly.com/lwc-third-party-libraries/)?** Yes, `Test.loadData` reads a CSV. It is good for large reference sets and bad for records you need to vary, because changing a value means editing a file you cannot see from the test.

---

Source: https://trailassembly.com/apex-test-data-factories/
Trail Assembly — independent Salesforce write-ups.
