@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.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add PranavNagrecha/AwesomeSalesforceSkills --skill apex-test-setup-patterns --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Apex Test Setup Patterns?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/pranavnagrecha-apex-test-setup-patterns)More formats (shields.io, HTML) on the badges page.
---
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 reusable test data factory class — use apex/test-data-factory-patterns. NOT for HTTP callout mocks — use apex/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"
- "testvisible runas mixed dml workaround apex test"
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.1.0
author: Pranav Nagrecha
updated: 2026-05-19
---
# 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:
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
| 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
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
- [ ] `@TestSetup` used for shared data (not repeated per method)
- [ ] Setup-object DML (User, Role, Group) isolated via `System.runAs`
- [ ] `Test.startTest/stopTest` wraps code-under-test
- [ ] Async jobs flushed at `stopTest`
- [ ] No `SeeAllData=true`
- [ ] `@TestVisible` used only where DI isn't feasible
- [ ] Setup method itself does not depend on org data
## 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
| 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 design
- `apex/apex-http-callout-mocking` — mocking HTTP in tests
- `apex/apex-system-runas` — user-context testing details
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!