name: apex-test-setup-patterns description: "@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 test data factory (use test-data-factory-patterns). NOT for mocking callouts (use apex-http-callout-mocking)." category: apex salesforce-version: "Spring '25+" well-architected-pillars:
- Reliability
- Performance tags:
- apex
- testing
- testsetup
- runas
- governor-limits triggers:
- "@testsetup method apex runs once per test class"
- "test setup data visibility across test methods"
- "test.startest test.stoptest governor limit reset"
- "@testvisible private field apex test access"
- "system.runas mixed dml setup vs hierarchy"
- "testsetup fails test class aborts all tests" inputs:
- Test class objective
- Shared data volume per test method
- User-context test requirements outputs:
- "@TestSetup block with factory calls"
- runAs scope plan
- startTest/stopTest governor reset placement dependencies: [] version: 1.0.0 author: Pranav Nagrecha updated: 2026-04-22
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
- Decide what goes in setup vs per-test. Setup data is rolled back after the class finishes, not after each method — but each method sees a fresh rollback-to-setup snapshot.
- Plan the runAs scope. Setup runs as the test-class user unless wrapped in
System.runAs. - Identify governor-reset needs. Any async/bulk work inside
Test.startTest()gets its own 100-callout / 100-SOQL / etc. budget.
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:
- Governor reset — isolates setup work from the code-under-test's limit budget.
- Async flush — future/Queueable/Batch jobs registered before
stopTestexecute synchronously whenstopTestis 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
| Situation | Approach |
|---|---|
| Multiple tests share identical data | @TestSetup |
| Each test needs unique/customized data | Per-method inline creation |
| Exercising async (future/Queueable/Batch) | Wrap in Test.startTest/stopTest |
| Need to test a different user's perspective | System.runAs(u) |
| Mixed setup + non-setup DML | System.runAs guard around setup-object DML |
| Override a private internal flag | @TestVisible |
Recommended Workflow
- Identify data common to every test method → move to
@TestSetup. - Create users in
@TestSetupvia aTestUserFactoryinsideSystem.runAs(new User(Id = UserInfo.getUserId()))to avoid mixed DML. - Per-method: wrap code-under-test in
Test.startTest()/Test.stopTest(). - For async, enqueue before
stopTest; assert results after. - For user-perspective tests, use
System.runAs(u) { ... }inside the method. - Use
@TestVisiblesparingly — prefer dependency injection via method params. - Never use
SeeAllData=trueon new tests.
Review Checklist
-
@TestSetupused for shared data (not repeated per method) - Setup-object DML (User, Role, Group) isolated via
System.runAs -
Test.startTest/stopTestwraps code-under-test - Async jobs flushed at
stopTest - No
SeeAllData=true -
@TestVisibleused only where DI isn't feasible - Setup method itself does not depend on org data
Salesforce-Specific Gotchas
- If
@TestSetupthrows, all test methods in the class fail. Keep setup focused; move optional data to per-method builders. Test.stopTest()resets limits only once per test method. You cannot nest start/stop pairs.@TestSetupruns once — not once per method. Static variables set inside setup persist across methods.- Mixed DML rule doesn't apply in
@TestSetupwhenSystem.runAsisn't present — the initial setup user context allows it. But if you enter arunAsblock you're back to mixed-DML constraints.
Output Artifacts
| Artifact | Description |
|---|---|
Test class with @TestSetup | Shared data block |
| runAs wrapper patterns | User-context isolation + mixed-DML guards |
Test.startTest/stopTest placement | Governor + async flush discipline |
Related Skills
apex/test-data-factory-patterns— shared data factory designapex/apex-http-callout-mocking— mocking HTTP in testsapex/apex-system-runas— user-context testing details