Generating Apex Tests
Generate production-ready Apex test classes and run disciplined test-fix loops with coverage analysis.
Core Principles
- One behavior per method — each test method validates a single scenario. Separate positive, negative, and bulk tests. NEVER combine related-but-distinct inputs (e.g., null and empty) in one method — create
_NullInput_ and _EmptyInput_ as separate test methods
- Bulkify tests — test with 251+ records to cross the 200-record trigger batch boundary. Batch Apex exception: in test context only one
execute() invocation runs, so set batchSize >= testRecordCount. See references/async-testing.md
- Isolate test data — every
@TestSetup must delegate record creation to a TestDataFactory class. If none exists, create one first. Never build record lists inline in @TestSetup. Never rely on org data (SeeAllData=false) or hardcoded IDs. For duplicate rule handling, see references/test-data-factory.md
- Assert meaningfully — use exact expected values computed from test data setup. NEVER use range assertions or approximate counts when the value is deterministic. Always include failure messages. See references/assertion-patterns.md
- Use
Assert class only — Assert.areEqual, Assert.isTrue, Assert.fail, etc. Never use legacy System.assert, System.assertEquals, or System.assertNotEquals
- Mock external boundaries — use
HttpCalloutMock for callouts, Test.setFixedSearchResults for SOSL, DML mock classes for database isolation. Design for testability via constructor injection. See references/mocking-patterns.md
- Test negative paths — validate error handling and exception scenarios, not just happy paths
- Wrap with start/stop — pair
Test.startTest() with Test.stopTest() to reset governor limits and force async execution
Test.startTest() / Test.stopTest()
Always wrap the code under test in Test.startTest() / Test.stopTest():
- Resets governor limits so the test measures only the code under test
- Executes async operations synchronously (queueables, batch, future methods)
- Fires scheduled jobs immediately
Test Code Anti-Patterns
| Anti-Pattern |
Fix |
| SOQL/DML inside loops |
Query once before the loop; use Map<Id, SObject> for lookups |
| Magic numbers in assertions |
Derive expected values from setup constants |
| God test class (>500 lines) |
Split into multiple test classes by behavior area |
| Long test methods (>30 lines) |
Extract Given/When/Then into helper methods |
Generic Exception catch |
Catch the specific expected type (e.g., DmlException) |
Workflow
Step 1 — Gather Context
Before generating or fixing tests, identify:
- the target production class(es) under test
- existing test classes, test data factories, and setup helpers
- desired test scope (single class, specific methods, suite, or local tests)
- coverage threshold (75% minimum for deploy, 90%+ recommended)
- org alias when running tests against an org
Step 2 — Generate the Test Class
Apply the structure, naming conventions, and patterns from the asset templates and reference docs.
MANDATORY — File Deliverables: For every test class, create BOTH files:
{ClassName}Test.cls — the test class (use assets/test-class-template.cls as starting point)
{ClassName}Test.cls-meta.xml — the metadata file:
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>66.0</apiVersion>
<status>Active</status>
</ApexClass>
If no TestDataFactory exists in the project, create TestDataFactory.cls + TestDataFactory.cls-meta.xml using assets/test-data-factory-template.cls.
@TestSetup Example
@TestSetup
static void setupTestData() {
List<Account> accounts = TestDataFactory.createAccounts(251, true);
}
Test Method Structure
Use Given/When/Then:
@isTest
static void shouldUpdateStatus_WhenValidInput() {
// Given
List<Account> accounts = [SELECT Id FROM Account];
// When
Test.startTest();
MyService.processAccounts(accounts);
Test.stopTest();
// Then
List<Account> updated = [SELECT Id, Status__c FROM Account];
Assert.areEqual(251, updated.size(), 'All accounts should be processed');
}
Negative Test — Exception Pattern
Use try/catch with Assert.fail to verify expected exceptions:
@isTest
static void shouldThrowException_WhenInvalidInput() {
// Given
List<Account> emptyList = new List<Account>();
// When/Then
Test.startTest();
try {
MyService.processAccounts(emptyList);
Assert.fail('Expected MyCustomException to be thrown');
} catch (MyCustomException e) {
Assert.isTrue(e.getMessage().contains('cannot be empty'),
'Exception message should indicate empty input');
}
Test.stopTest();
}
Naming Convention
should[ExpectedResult]_When[Scenario]: shouldSendNotification_WhenOpportunityClosedWon
[SubjectOrAction]_[Scenario]_[ExpectedResult]: AccountUpdate_ChangeName_Success
Step 3 — Run Tests
Start narrow when debugging; widen after the fix is stable.
# Single test class
sf apex run test --class-names MyServiceTest --result-format human --code-coverage --target-org <alias>
# Specific test methods
sf apex run test --tests MyServiceTest.shouldUpdateStatus_WhenValidInput --result-format human --target-org <alias>
# All local tests
sf apex run test --test-level RunLocalTests --result-format human --code-coverage --target-org <alias>
Step 4 — Analyze Results
Focus on:
- failing methods — exception types and stack traces
- uncovered lines and weak coverage areas
- whether failures indicate bad test data, brittle assertions, or broken production logic
- when class metadata crosses from versions 66.0 and below to 67.0 and above, check explicit sharing, decide user/system mode per operation, grant required CRUD/FLS in test users or permission sets, and rerun affected tests
Step 5 — Fix Loop
When tests fail, run a disciplined fix loop (max 3 iterations — stop and surface root cause if still failing):
- Read the failing test class and the class under test
- Identify root cause from error messages and stack traces
- Apply fix — adjust test data or assertions for test-side issues; delegate production code issues to the
platform-apex-generate skill
- Rerun the focused test before broader regression
- Repeat until all tests pass, iteration limit reached, or root cause requires design change
Step 6 — Validate Coverage
| Level |
Coverage |
Purpose |
| Production deploy |
75% minimum |
Required by Salesforce |
| Recommended |
90%+ |
Best practice target |
| Critical paths |
100% |
Business-critical code |
Cover all paths: positive, negative/exception, bulk (251+ records), callout/async.
What to Test by Component
| Component |
Key Test Scenarios |
| Trigger |
Bulk insert/update/delete, recursion guard, field change detection |
| Service |
Valid/invalid inputs, bulk operations, exception handling |
| Controller |
Page load, action methods, view state |
| Batch |
start/execute/finish, scope matching (batch size >= record count), Database.Stateful tracking, error handling, chaining (separate methods — finish() calling Database.executeBatch() throws UnexpectedException) |
| Queueable |
Chaining (only first job runs in tests), bulkification, error handling, callout mocks before Test.startTest() |
| Callout |
Success response, error response, timeout |
| Selector |
Valid/null/empty inputs, bulk (251+), field population, sort order, WITH USER_MODE via System.runAs |
| Scheduled |
Direct execution via execute(null), CRON registration via CronTrigger query |
| Platform Event |
Test.enableChangeDataCapture(), Test.getEventBus().deliver(), verify subscriber side effects |
Output Expectations
Deliverables per test class:
{ClassName}Test.cls + {ClassName}Test.cls-meta.xml (match API version of class under test; default 66.0)
TestDataFactory.cls + TestDataFactory.cls-meta.xml (if not already present)
Reference Files
Load on demand for detailed patterns:
| Reference |
When to use |
| references/test-data-factory.md |
TestDataFactory patterns, field overrides, duplicate rule handling |
| references/assertion-patterns.md |
Assertion best practices, anti-patterns, common pitfalls |
| references/mocking-patterns.md |
HttpCalloutMock, DML mocking, StubProvider, SOSL, Email, Platform Events |
| references/async-testing.md |
Batch, Queueable, Future, Scheduled job testing |
1---
2name: platform-apex-test-generate
3description: "Generate and validate Apex test classes with TestDataFactory patterns, bulk testing (251+ records), mocking strategies, assertion best practices, and disciplined test-fix loops. Use this skill when creating new Apex test classes, improving test coverage, debugging and fixing failing Apex tests, running test execution and coverage analysis, or implementing testing patterns for triggers, services, controllers, batch jobs, queueables, and integrations. Triggers on *Test.cls, *_Test.cls files, sf apex run test workflows, coverage reports, test-fix loops. Do NOT trigger for production Apex code (use platform-apex-generate) or Jest/LWC tests."
4metadata:
5 version: "1.1"
6 domains: ["Platform"]
7 minApiVersion: "66.0"
8 relatedSkills:
9 - "platform-apex-generate"
10 cliTools:
11 - tool: ["sf"]
12 semver: ">=2.0.0"
13---
14
15# Generating Apex Tests
16
17Generate production-ready Apex test classes and run disciplined test-fix loops with coverage analysis.
18
19## Core Principles
20
211. **One behavior per method** — each test method validates a single scenario. Separate positive, negative, and bulk tests. NEVER combine related-but-distinct inputs (e.g., null and empty) in one method — create `_NullInput_` and `_EmptyInput_` as separate test methods
222. **Bulkify tests** — test with 251+ records to cross the 200-record trigger batch boundary. **Batch Apex exception:** in test context only one `execute()` invocation runs, so set `batchSize >= testRecordCount`. See [references/async-testing.md](references/async-testing.md)
233. **Isolate test data** — every `@TestSetup` must delegate record creation to a `TestDataFactory` class. If none exists, create one first. Never build record lists inline in `@TestSetup`. Never rely on org data (`SeeAllData=false`) or hardcoded IDs. For duplicate rule handling, see [references/test-data-factory.md](references/test-data-factory.md)
244. **Assert meaningfully** — use exact expected values computed from test data setup. NEVER use range assertions or approximate counts when the value is deterministic. Always include failure messages. See [references/assertion-patterns.md](references/assertion-patterns.md)
255. **Use `Assert` class only** — `Assert.areEqual`, `Assert.isTrue`, `Assert.fail`, etc. Never use legacy `System.assert`, `System.assertEquals`, or `System.assertNotEquals`
266. **Mock external boundaries** — use `HttpCalloutMock` for callouts, `Test.setFixedSearchResults` for SOSL, DML mock classes for database isolation. Design for testability via constructor injection. See [references/mocking-patterns.md](references/mocking-patterns.md)
277. **Test negative paths** — validate error handling and exception scenarios, not just happy paths
288. **Wrap with start/stop** — pair `Test.startTest()` with `Test.stopTest()` to reset governor limits and force async execution
29
30## Test.startTest() / Test.stopTest()
31
32Always wrap the code under test in `Test.startTest()` / `Test.stopTest()`:
33
34- Resets governor limits so the test measures only the code under test
35- Executes async operations synchronously (queueables, batch, future methods)
36- Fires scheduled jobs immediately
37
38## Test Code Anti-Patterns
39
40| Anti-Pattern | Fix |
41|---|---|
42| SOQL/DML inside loops | Query once before the loop; use `Map<Id, SObject>` for lookups |
43| Magic numbers in assertions | Derive expected values from setup constants |
44| God test class (>500 lines) | Split into multiple test classes by behavior area |
45| Long test methods (>30 lines) | Extract Given/When/Then into helper methods |
46| Generic `Exception` catch | Catch the specific expected type (e.g., `DmlException`) |
47
48## Workflow
49
50### Step 1 — Gather Context
51
52Before generating or fixing tests, identify:
53
54- the target production class(es) under test
55- existing test classes, test data factories, and setup helpers
56- desired test scope (single class, specific methods, suite, or local tests)
57- coverage threshold (75% minimum for deploy, 90%+ recommended)
58- org alias when running tests against an org
59
60### Step 2 — Generate the Test Class
61
62Apply the structure, naming conventions, and patterns from the asset templates and reference docs.
63
64**MANDATORY — File Deliverables:** For every test class, create BOTH files:
651. `{ClassName}Test.cls` — the test class (use [assets/test-class-template.cls](assets/test-class-template.cls) as starting point)
662. `{ClassName}Test.cls-meta.xml` — the metadata file:
67
68```xml
69<?xml version="1.0" encoding="UTF-8"?>
70<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
71 <apiVersion>66.0</apiVersion>
72 <status>Active</status>
73</ApexClass>
74```
75
76If no `TestDataFactory` exists in the project, create `TestDataFactory.cls` + `TestDataFactory.cls-meta.xml` using [assets/test-data-factory-template.cls](assets/test-data-factory-template.cls).
77
78#### @TestSetup Example
79
80```apex
81@TestSetup
82static void setupTestData() {
83 List<Account> accounts = TestDataFactory.createAccounts(251, true);
84}
85```
86
87#### Test Method Structure
88
89Use Given/When/Then:
90
91```apex
92@isTest
93static void shouldUpdateStatus_WhenValidInput() {
94 // Given
95 List<Account> accounts = [SELECT Id FROM Account];
96
97 // When
98 Test.startTest();
99 MyService.processAccounts(accounts);
100 Test.stopTest();
101
102 // Then
103 List<Account> updated = [SELECT Id, Status__c FROM Account];
104 Assert.areEqual(251, updated.size(), 'All accounts should be processed');
105}
106```
107
108#### Negative Test — Exception Pattern
109
110Use try/catch with `Assert.fail` to verify expected exceptions:
111
112```apex
113@isTest
114static void shouldThrowException_WhenInvalidInput() {
115 // Given
116 List<Account> emptyList = new List<Account>();
117
118 // When/Then
119 Test.startTest();
120 try {
121 MyService.processAccounts(emptyList);
122 Assert.fail('Expected MyCustomException to be thrown');
123 } catch (MyCustomException e) {
124 Assert.isTrue(e.getMessage().contains('cannot be empty'),
125 'Exception message should indicate empty input');
126 }
127 Test.stopTest();
128}
129```
130
131#### Naming Convention
132
133- `should[ExpectedResult]_When[Scenario]`: `shouldSendNotification_WhenOpportunityClosedWon`
134- `[SubjectOrAction]_[Scenario]_[ExpectedResult]`: `AccountUpdate_ChangeName_Success`
135
136### Step 3 — Run Tests
137
138Start narrow when debugging; widen after the fix is stable.
139
140```bash
141# Single test class
142sf apex run test --class-names MyServiceTest --result-format human --code-coverage --target-org <alias>
143
144# Specific test methods
145sf apex run test --tests MyServiceTest.shouldUpdateStatus_WhenValidInput --result-format human --target-org <alias>
146
147# All local tests
148sf apex run test --test-level RunLocalTests --result-format human --code-coverage --target-org <alias>
149```
150
151### Step 4 — Analyze Results
152
153Focus on:
154
155- failing methods — exception types and stack traces
156- uncovered lines and weak coverage areas
157- whether failures indicate bad test data, brittle assertions, or broken production logic
158- when class metadata crosses from versions 66.0 and below to 67.0 and above, check explicit sharing, decide user/system mode per operation, grant required CRUD/FLS in test users or permission sets, and rerun affected tests
159
160### Step 5 — Fix Loop
161
162When tests fail, run a disciplined fix loop (max 3 iterations — stop and surface root cause if still failing):
163
1641. Read the failing test class and the class under test
1652. Identify root cause from error messages and stack traces
1663. Apply fix — adjust test data or assertions for test-side issues; delegate production code issues to the `platform-apex-generate` skill
1674. Rerun the focused test before broader regression
1685. Repeat until all tests pass, iteration limit reached, or root cause requires design change
169
170### Step 6 — Validate Coverage
171
172| Level | Coverage | Purpose |
173|-------|----------|---------|
174| Production deploy | 75% minimum | Required by Salesforce |
175| Recommended | 90%+ | Best practice target |
176| Critical paths | 100% | Business-critical code |
177
178Cover all paths: positive, negative/exception, bulk (251+ records), callout/async.
179
180## What to Test by Component
181
182| Component | Key Test Scenarios |
183|-----------|-------------------|
184| Trigger | Bulk insert/update/delete, recursion guard, field change detection |
185| Service | Valid/invalid inputs, bulk operations, exception handling |
186| Controller | Page load, action methods, view state |
187| Batch | start/execute/finish, scope matching (batch size >= record count), `Database.Stateful` tracking, error handling, chaining (separate methods — `finish()` calling `Database.executeBatch()` throws `UnexpectedException`) |
188| Queueable | Chaining (only first job runs in tests), bulkification, error handling, callout mocks before `Test.startTest()` |
189| Callout | Success response, error response, timeout |
190| Selector | Valid/null/empty inputs, bulk (251+), field population, sort order, `WITH USER_MODE` via `System.runAs` |
191| Scheduled | Direct execution via `execute(null)`, CRON registration via `CronTrigger` query |
192| Platform Event | `Test.enableChangeDataCapture()`, `Test.getEventBus().deliver()`, verify subscriber side effects |
193
194## Output Expectations
195
196Deliverables per test class:
197- `{ClassName}Test.cls` + `{ClassName}Test.cls-meta.xml` (match API version of class under test; default `66.0`)
198- `TestDataFactory.cls` + `TestDataFactory.cls-meta.xml` (if not already present)
199
200## Reference Files
201
202Load on demand for detailed patterns:
203
204| Reference | When to use |
205|-----------|-------------|
206| [references/test-data-factory.md](references/test-data-factory.md) | TestDataFactory patterns, field overrides, duplicate rule handling |
207| [references/assertion-patterns.md](references/assertion-patterns.md) | Assertion best practices, anti-patterns, common pitfalls |
208| [references/mocking-patterns.md](references/mocking-patterns.md) | HttpCalloutMock, DML mocking, StubProvider, SOSL, Email, Platform Events |
209| [references/async-testing.md](references/async-testing.md) | Batch, Queueable, Future, Scheduled job testing |