Org Skills
Referenceapex-testing

TestDataFactory — builder pattern

Supporting material for apex-testing. Agents load it on demand; it ships inside the skill folder.

RawSource

Target: Spring '26. One @IsTest public class TestDataFactory per project; builders set every required field so tests never fail on a new validation rule for a field they don't care about.

@IsTest
public class TestDataFactory {

    /* ---------- Account ---------- */
    public static AccountBuilder account() { return new AccountBuilder(); }

    public class AccountBuilder {
        private final Account record = new Account(Name = 'Test Account ' + Crypto.getRandomInteger(), Industry = 'Technology');
        public AccountBuilder withName(String name) { record.Name = name; return this; }
        public AccountBuilder withRecordType(String developerName) {
            record.RecordTypeId = Utils.getRecordTypeId('Account:' + developerName);
            return this;
        }
        public Account build() { return record; }
        public Account persist() { Database.insert(record); return record; }
    }

    /* ---------- Orders (bulk) ---------- */
    public static OrderListBuilder orders(Id accountId, Integer count) { return new OrderListBuilder(accountId, count); }

    public class OrderListBuilder {
        private final List<Order__c> records = new List<Order__c>();
        OrderListBuilder(Id accountId, Integer count) {
            for (Integer i = 0; i < count; i++) {
                records.add(new Order__c(Account__c = accountId, Amount__c = 100, Status__c = 'Draft', Name = 'ORD-' + i));
            }
        }
        public OrderListBuilder withStatus(String status) { for (Order__c o : records) { o.Status__c = status; } return this; }
        public List<Order__c> build() { return records; }
        public List<Order__c> persist() { Database.insert(records); return records; }
    }

    /* ---------- Users ---------- */
    public static User user(String profileName) {
        Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
        String unique = String.valueOf(Crypto.getRandomInteger()).replace('-', '');
        User u = new User(
            ProfileId = p.Id, Username = 'test.' + unique + '@example.com', Email = 'test@example.com',
            Alias = 'tu' + unique.left(5), LastName = 'Tester', TimeZoneSidKey = 'GMT', LocaleSidKey = 'en_US',
            EmailEncodingKey = 'UTF-8', LanguageLocaleKey = 'en_US');
        System.runAs(new User(Id = UserInfo.getUserId())) { Database.insert(u); }   // avoids MIXED_DML_OPERATION
        return u;
    }

    public static void assignPermissionSet(Id userId, String permissionSetName) {
        PermissionSet ps = [SELECT Id FROM PermissionSet WHERE Name = :permissionSetName LIMIT 1];
        System.runAs(new User(Id = UserInfo.getUserId())) {
            Database.insert(new PermissionSetAssignment(AssigneeId = userId, PermissionSetId = ps.Id));
        }
    }
}

Rules

  • Required fields and record types live in the builder, never in the test method. When a new required field appears, one edit fixes every test.
  • persist() does one DML for the whole list — the factory itself must be bulk-safe.
  • Use Crypto.getRandomInteger() for uniqueness; DateTime.now().getTime() collides in parallel runs.
  • Profile names differ by org and edition; "Minimum Access - Salesforce" exists in every modern org and is the right base for permission-set-driven negative tests.
  • Never query production data (SeeAllData) to "save time"; it makes tests order-dependent and blocks deployment when data drifts.
  • Put the factory in the same package as the tests; a shared TestDataFactory across unlocked packages must be global and versioned like any API.