SfSkills Domains Docs GitHub

Home / apex / apex-test-setup-patterns

apex-test-setup-patterns

@TestSetup method semantics: one-time creation per test class, isolation behavior, @TestVisible, System.runAs, Test.startTest/stopTest governor reset, mixed-DML boundaries. NOT for building a reusable test data factory class — use apex/test-data-factory-patterns. NOT for HTTP callout mocks — use apex/apex-http-callout-mocking.

Open the skill folder on GitHub


Apex Test Setup Patterns

Activate when writing or reviewing an Apex test class. @TestSetup controls one-time data creation shared across every test method, Test.startTest()/Test.stopTest() define the governor-reset boundary, and System.runAs defines which user the test impersonates. Getting any of these wrong produces tests that pass flakily, misreport coverage, or exercise the wrong user context.

Before Starting

Core Concepts

@TestSetup

@IsTest
private class AccountServiceTest {
    @TestSetup
    static void setup() {
        Account[] accs = new List<Account>{
            new Account(Name = 'A1'),
            new Account(Name = 'A2')
        };
        insert accs;
    }

    @IsTest static void testFoo() {
        Account a = [SELECT Id FROM Account WHERE Name = 'A1'];
        // ...
    }
}

Runs once per test class before any test method. Each test method starts with setup-state data; changes made inside a test method are rolled back after that method completes.

Test.startTest() / Test.stopTest()

Test.startTest();
// Code inside gets a fresh set of governor limits.
// Any async jobs enqueued here (Queueable, future, Batch) run synchronously at stopTest().
Test.stopTest();

Critical for two reasons:

  1. Governor reset — isolates setup work from the code-under-test's limit budget.
  2. Async flush — future/Queueable/Batch jobs registered before stopTest execute synchronously when stopTest is called.

@TestVisible

Annotation on a private member that makes it visible to test classes (but NOT to production code). Use when tests need to inject state or call a helper that shouldn't be public.

public class OrderService {
    @TestVisible private static Integer retryCount = 3;
    @TestVisible private static Boolean simulateFailure = false;
}

System.runAs

User u = [SELECT Id FROM User WHERE Profile.Name = 'Standard User' LIMIT 1];
System.runAs(u) {
    // DML as that user; CRUD/FLS/Sharing enforced per their profile
}

Also the only way around mixed DML — setup-object DML (User, UserRole, Group) cannot coexist with non-setup DML in a test unless isolated via System.runAs(new User(Id = UserInfo.getUserId())).

Mixed DML workaround

System.runAs(new User(Id = UserInfo.getUserId())) {
    insert new User(...);  // setup-object DML
}
insert new Account(...);    // non-setup DML — now legal

Common Patterns

Pattern: Setup with runAs for ownership

@TestSetup
static void setup() {
    User u = TestUserFactory.createStandardUser();
    insert u;
    System.runAs(u) {
        insert new Account(Name = 'Owned by u');
    }
}

Pattern: startTest for async flush

@IsTest static void testQueueable() {
    Test.startTest();
    System.enqueueJob(new MyQueueable());
    Test.stopTest();  // Queueable runs synchronously here
    System.assertEquals(1, [SELECT COUNT() FROM Task]);
}

Pattern: @TestVisible injection for failure simulation

@IsTest static void testRetryOnFailure() {
    OrderService.simulateFailure = true;  // @TestVisible flag
    // assert retries
}

Decision Guidance

SituationApproach
Multiple tests share identical data@TestSetup
Each test needs unique/customized dataPer-method inline creation
Exercising async (future/Queueable/Batch)Wrap in Test.startTest/stopTest
Need to test a different user's perspectiveSystem.runAs(u)
Mixed setup + non-setup DMLSystem.runAs guard around setup-object DML
Override a private internal flag@TestVisible
  1. Identify data common to every test method → move to @TestSetup.
  2. Create users in @TestSetup via a TestUserFactory inside System.runAs(new User(Id = UserInfo.getUserId())) to avoid mixed DML.
  3. Per-method: wrap code-under-test in Test.startTest() / Test.stopTest().
  4. For async, enqueue before stopTest; assert results after.
  5. For user-perspective tests, use System.runAs(u) { ... } inside the method.
  6. Use @TestVisible sparingly — prefer dependency injection via method params.
  7. Never use SeeAllData=true on new tests.

Review Checklist

Salesforce-Specific Gotchas

  1. If @TestSetup throws, all test methods in the class fail. Keep setup focused; move optional data to per-method builders.
  2. Test.stopTest() resets limits only once per test method. You cannot nest start/stop pairs.
  3. @TestSetup runs once — not once per method. Static variables set inside setup persist across methods.
  4. Mixed DML rule doesn't apply in @TestSetup when System.runAs isn't present — the initial setup user context allows it. But if you enter a runAs block you're back to mixed-DML constraints.

Output Artifacts

ArtifactDescription
Test class with @TestSetupShared data block
runAs wrapper patternsUser-context isolation + mixed-DML guards
Test.startTest/stopTest placementGovernor + async flush discipline

Package files