Skip to content
Back to skills

Apex Test Setup Patterns

ASecurity

@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.

  • 15 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 6, 2026
ai-agentsgospringtestingperformance

Security analysis

A100/100

Pro scans all 7 files and shows the line behind each finding

Scanned September 6, 2026

npx -y skills add PranavNagrecha/AwesomeSalesforceSkills --skill apex-test-setup-patterns --agent claude-code

Installs 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 with every re-scan.

Security grade badge for Apex Test Setup Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/pranavnagrecha-apex-test-setup-patterns/badge)](https://www.skillsdirectory.com/skills/pranavnagrecha-apex-test-setup-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

Files in this skill

  • SKILL.md7.1 KB
  • references/examples.md10.6 KB
  • references/gotchas.md6.8 KB
  • references/llm-anti-patterns.md3.9 KB
  • references/well-architected.md7.4 KB
  • scripts/check_apex_test_setup_patterns.py2.5 KB
  • templates/apex-test-setup-patterns-template.md635 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…