Salesforce Apex Test Generation, Review, and Execution
Expert Apex test engineer. Generate meaningful tests — not coverage padding —
with proper isolation, bulk safety, permission awareness, and assertions that
would actually catch regressions. Deploy and run via a Salesforce MCP server.
Dispatch
Parse $ARGUMENTS to determine which workflow to run:
| First argument or intent |
Workflow |
generate, "write tests for X" |
Generate Tests |
review, "score my tests", "are these good" |
Review Tests |
run, "run the tests", "what's my coverage" |
Run Tests |
| No argument |
Ask which workflow; if a class name is given, default to Generate |
Execution modes
Detect the execution mode once per session — see
references/execution-modes.md. In sfdx-repo mode, read classes from disk;
otherwise fetch via MCP. Initialize the org connection first (org_init — your
MCP server's session-init step; if none exists, verify with
SELECT Id FROM Organization LIMIT 1).
Generate Tests
Phase 1: Understand the code under test
- Fetch the target class/trigger body (Tooling API query on
ApexClass /
ApexTrigger, or read from disk in sfdx-repo mode).
- Map the surface: public/global methods,
@AuraEnabled, @InvocableMethod,
@future, Queueable/Batchable/Schedulable interfaces, callouts
(Http, HttpRequest, named credentials), DML and SOQL operations,
custom exceptions, and branching logic.
- Fetch dependencies that affect test data: required fields, validation
rules, and record types on the objects touched (
sobject_describe /
metadata read). Tests fail in real orgs because of validation rules the
generator never saw — check them up front.
Phase 2: Design the test plan
Build a test matrix BEFORE writing code. Cover every row that applies:
| Dimension |
What to include |
| Positive path |
Each public method's happy path with realistic data |
| Negative path |
Invalid inputs, empty lists, nulls, exception branches asserted via try/catch or Assert.fail() sentinel |
| Bulk |
200-record operations through the same code path |
| Permissions |
System.runAs() with a minimally-privileged user where the code enforces CRUD/FLS or sharing |
| Async |
Test.startTest()/stopTest() to flush futures/queueables; Test.getEventBus().deliver() for platform events |
| Callouts |
HttpCalloutMock / WebServiceMock implementations per external call |
| Boundary |
Governor-relevant sizes, date boundaries, recursion guards |
Present the matrix to the user when scope is ambiguous; otherwise proceed.
Phase 3: Generate the test class
Rules that make tests worth having:
- One test class per production class, named
<ClassName>Test.
@IsTest annotation, private, API version matching the class under test.
@TestSetup for shared data; a TestDataFactory pattern
(see references/test-patterns.md) when 3+ test classes need the same
objects. A copyable starting file lives at
../../shared/templates/TestDataFactory.cls — copy it and extend
per object; the reference doc explains the pattern. Never
SeeAllData=true unless testing something that genuinely
requires org data (document why in a comment).
- Modern
Assert class (Assert.areEqual, Assert.isTrue,
Assert.fail) with a message on every assertion explaining what broke.
Every test method asserts something meaningful — method-ran-without-error
is not a test.
Test.startTest()/stopTest() around the action under test only — not
around data setup — so governor limits reset where it matters and async
work flushes.
- Bulk test proves bulkification: build 200 records, run the path, assert
on ALL results (not just the first record), and keep it under limits.
- runAs tests prove security: create a user with a minimal profile or
permission set,
System.runAs() the restricted path, assert the
enforcement (exception or stripped fields) actually happens.
- No dependencies between test methods. Each method stands alone.
Phase 4: Deploy
Deploy the test class via the MCP metadata tool (metadata_create /
metadata_update equivalents). On compile errors, fix and redeploy — common
causes are missing required fields on test data (re-check Phase 1 findings),
API-version mismatches, and SOQL WHERE clauses on non-filterable fields
(long text areas can't be filtered in SOQL — query without the filter and
filter in memory instead). When you get to running the tests, remember
runTestsAsynchronous is POST-only — a GET returns 405.
Phase 5: Run and report
Run the new tests (see Run Tests below), then report: pass/fail per method,
coverage delta on the class under test, and the test-quality score
(see Scoring). Flag any class still below 75% coverage — the deployment
minimum — and below 85%, the recommended bar for AppExchange submissions.
Review Tests
Score one or more existing test classes against the 120-point rubric.
- Fetch the test class body plus the production classes it covers.
- Evaluate each rubric category (below); cite line-level evidence for every
deduction — "no message on Assert at line 42", not "assertions could be
better".
- Output a scored report:
| Test class |
Score |
% |
Verdict |
Top issues |
| FooTest |
96/120 |
80% |
✅ Pass |
No runAs coverage; 2 assert-free methods |
Threshold: 80/120 (67%) passes; below that, list the remediation plan in
priority order (missing assertions first — they're the cheapest fix with the
highest regression-catching value).
Test-Quality Scoring (120 points)
| Category |
Points |
What earns them |
| Assertion quality |
25 |
Every method asserts outcomes with messages; negative paths assert exceptions; no assert-free methods |
| Data setup & isolation |
20 |
@TestSetup / factory pattern; no SeeAllData; required fields handled; no inter-test dependencies |
| Bulk & governor safety |
20 |
200-record path exercised; assertions over the full set; startTest/stopTest placed correctly |
| Coverage breadth |
20 |
All public methods + branches; boundary cases; not just the happy path |
| Permission & sharing |
15 |
runAs with restricted user where code enforces security; both allow and deny asserted |
| Async & callout handling |
15 |
Mocks for every callout; async flushed and results asserted; platform events delivered |
| Structure & naming |
5 |
Test naming; one concern per method; readable arrange-act-assert |
Scoring notes: a test class with high line coverage but weak assertions caps
at 60/120 — coverage without assertions is the most expensive false comfort
in Salesforce development, and the rubric is deliberately built to punish it.
Run Tests
MCP-first execution via the Tooling API (works in every execution mode; in
cli mode sf apex run test is the alternate path):
- Queue the run — Tooling API
runTestsAsynchronous with the test class
IDs, or insert an ApexTestQueueItem per class via Tooling DML.
- Poll status —
SELECT Id, Status, ApexClassId FROM ApexTestQueueItem WHERE ParentJobId = '<jobId>' until all rows are Completed, Failed,
or Aborted.
- Fetch results —
SELECT Outcome, MethodName, Message, StackTrace, ApexClass.Name, RunTime FROM ApexTestResult WHERE AsyncApexJobId = '<jobId>'.
- Fetch coverage —
SELECT ApexClassOrTrigger.Name, NumLinesCovered, NumLinesUncovered FROM ApexCodeCoverageAggregate WHERE ApexClassOrTrigger.Name IN (...) (Tooling API).
- Report — pass/fail table, failure messages with stack traces, coverage
per class, org-wide coverage if requested. For failures, offer to hand off
to
sf-debug (log analysis) or sf-apex (fix the production code).
Never run tests you just generated against a production org without telling
the user — test runs consume async capacity and coverage recalculation can be
slow in large orgs. Prefer sandboxes when the user has one connected.
Cross-skill handoffs
- Production code fixes or refactors → sf-apex (150-point rubric)
- Test failures needing log analysis → sf-debug
- Coverage strategy for an AppExchange submission → sf-security
(review-readiness checklist includes the 75% bar and test quality)
References
| File |
Read when |
references/test-patterns.md |
Writing factories, mocks, async/event tests, runAs patterns |
references/execution-modes.md |
Start of session — detect mode |
1---2name: sf-test3description: Generates, reviews, and runs Salesforce Apex test classes with a 120-point test-quality scoring rubric using a Salesforce MCP server. Use when writing test classes, improving code coverage, reviewing existing tests for assertion quality and bulk safety, running test suites, or diagnosing coverage gaps before a deployment or AppExchange submission. Do NOT use for writing or refactoring production Apex (use sf-apex), diagnosing runtime failures from debug logs (use sf-debug), or scanning code for security vulnerabilities (use sf-security). Usage: /sf-test [generate|review|run] [class|all] {name} ...4---56# Salesforce Apex Test Generation, Review, and Execution78Expert Apex test engineer. Generate meaningful tests — not coverage padding —9with proper isolation, bulk safety, permission awareness, and assertions that10would actually catch regressions. Deploy and run via a Salesforce MCP server.1112## Dispatch1314Parse `$ARGUMENTS` to determine which workflow to run:1516| First argument or intent | Workflow |17| --------------------------------------------- | ------------- |18| `generate`, "write tests for X" | Generate Tests |19| `review`, "score my tests", "are these good" | Review Tests |20| `run`, "run the tests", "what's my coverage" | Run Tests |21| No argument | Ask which workflow; if a class name is given, default to Generate |2223## Execution modes2425Detect the execution mode once per session — see26`references/execution-modes.md`. In `sfdx-repo` mode, read classes from disk;27otherwise fetch via MCP. Initialize the org connection first (`org_init` — your28MCP server's session-init step; if none exists, verify with29`SELECT Id FROM Organization LIMIT 1`).3031---3233## Generate Tests3435### Phase 1: Understand the code under test36371. Fetch the target class/trigger body (Tooling API query on `ApexClass` /38 `ApexTrigger`, or read from disk in `sfdx-repo` mode).392. Map the surface: public/global methods, `@AuraEnabled`, `@InvocableMethod`,40 `@future`, Queueable/Batchable/Schedulable interfaces, callouts41 (`Http`, `HttpRequest`, named credentials), DML and SOQL operations,42 custom exceptions, and branching logic.433. Fetch dependencies that affect test data: required fields, validation44 rules, and record types on the objects touched (`sobject_describe` /45 metadata read). Tests fail in real orgs because of validation rules the46 generator never saw — check them up front.4748### Phase 2: Design the test plan4950Build a test matrix BEFORE writing code. Cover every row that applies:5152| Dimension | What to include |53| ---------------- | ------------------------------------------------------------------ |54| Positive path | Each public method's happy path with realistic data |55| Negative path | Invalid inputs, empty lists, nulls, exception branches asserted via try/catch or `Assert.fail()` sentinel |56| Bulk | 200-record operations through the same code path |57| Permissions | `System.runAs()` with a minimally-privileged user where the code enforces CRUD/FLS or sharing |58| Async | `Test.startTest()/stopTest()` to flush futures/queueables; `Test.getEventBus().deliver()` for platform events |59| Callouts | `HttpCalloutMock` / `WebServiceMock` implementations per external call |60| Boundary | Governor-relevant sizes, date boundaries, recursion guards |6162Present the matrix to the user when scope is ambiguous; otherwise proceed.6364### Phase 3: Generate the test class6566Rules that make tests worth having:6768- **One test class per production class**, named `<ClassName>Test`.69 `@IsTest` annotation, `private`, API version matching the class under test.70- **`@TestSetup` for shared data**; a `TestDataFactory` pattern71 (see `references/test-patterns.md`) when 3+ test classes need the same72 objects. A copyable starting file lives at73 `../../shared/templates/TestDataFactory.cls` — copy it and extend74 per object; the reference doc explains the pattern. Never75 `SeeAllData=true` unless testing something that genuinely76 requires org data (document why in a comment).77- **Modern `Assert` class** (`Assert.areEqual`, `Assert.isTrue`,78 `Assert.fail`) with a message on every assertion explaining what broke.79 Every test method asserts something meaningful — method-ran-without-error80 is not a test.81- **`Test.startTest()/stopTest()`** around the action under test only — not82 around data setup — so governor limits reset where it matters and async83 work flushes.84- **Bulk test proves bulkification**: build 200 records, run the path, assert85 on ALL results (not just the first record), and keep it under limits.86- **runAs tests prove security**: create a user with a minimal profile or87 permission set, `System.runAs()` the restricted path, assert the88 enforcement (exception or stripped fields) actually happens.89- **No dependencies between test methods.** Each method stands alone.9091### Phase 4: Deploy9293Deploy the test class via the MCP metadata tool (`metadata_create` /94`metadata_update` equivalents). On compile errors, fix and redeploy — common95causes are missing required fields on test data (re-check Phase 1 findings),96API-version mismatches, and SOQL `WHERE` clauses on non-filterable fields97(long text areas can't be filtered in SOQL — query without the filter and98filter in memory instead). When you get to running the tests, remember99`runTestsAsynchronous` is POST-only — a GET returns 405.100101### Phase 5: Run and report102103Run the new tests (see Run Tests below), then report: pass/fail per method,104coverage delta on the class under test, and the test-quality score105(see Scoring). Flag any class still below 75% coverage — the deployment106minimum — and below 85%, the recommended bar for AppExchange submissions.107108---109110## Review Tests111112Score one or more existing test classes against the 120-point rubric.1131141. Fetch the test class body plus the production classes it covers.1152. Evaluate each rubric category (below); cite line-level evidence for every116 deduction — "no message on Assert at line 42", not "assertions could be117 better".1183. Output a scored report:119120| Test class | Score | % | Verdict | Top issues |121| ---------- | ----- | -- | ------- | ---------- |122| FooTest | 96/120 | 80% | ✅ Pass | No runAs coverage; 2 assert-free methods |123124Threshold: 80/120 (67%) passes; below that, list the remediation plan in125priority order (missing assertions first — they're the cheapest fix with the126highest regression-catching value).127128## Test-Quality Scoring (120 points)129130| Category | Points | What earns them |131| ------------------------- | ------ | ---------------------------------------------------------------------- |132| Assertion quality | 25 | Every method asserts outcomes with messages; negative paths assert exceptions; no assert-free methods |133| Data setup & isolation | 20 | @TestSetup / factory pattern; no SeeAllData; required fields handled; no inter-test dependencies |134| Bulk & governor safety | 20 | 200-record path exercised; assertions over the full set; startTest/stopTest placed correctly |135| Coverage breadth | 20 | All public methods + branches; boundary cases; not just the happy path |136| Permission & sharing | 15 | runAs with restricted user where code enforces security; both allow and deny asserted |137| Async & callout handling | 15 | Mocks for every callout; async flushed and results asserted; platform events delivered |138| Structure & naming | 5 | <ClassName>Test naming; one concern per method; readable arrange-act-assert |139140Scoring notes: a test class with high line coverage but weak assertions caps141at 60/120 — coverage without assertions is the most expensive false comfort142in Salesforce development, and the rubric is deliberately built to punish it.143144---145146## Run Tests147148MCP-first execution via the Tooling API (works in every execution mode; in149`cli` mode `sf apex run test` is the alternate path):1501511. **Queue the run** — Tooling API `runTestsAsynchronous` with the test class152 IDs, or insert an `ApexTestQueueItem` per class via Tooling DML.1532. **Poll status** — `SELECT Id, Status, ApexClassId FROM ApexTestQueueItem154 WHERE ParentJobId = '<jobId>'` until all rows are `Completed`, `Failed`,155 or `Aborted`.1563. **Fetch results** — `SELECT Outcome, MethodName, Message, StackTrace,157 ApexClass.Name, RunTime FROM ApexTestResult WHERE AsyncApexJobId =158 '<jobId>'`.1594. **Fetch coverage** — `SELECT ApexClassOrTrigger.Name, NumLinesCovered,160 NumLinesUncovered FROM ApexCodeCoverageAggregate WHERE161 ApexClassOrTrigger.Name IN (...)` (Tooling API).1625. **Report** — pass/fail table, failure messages with stack traces, coverage163 per class, org-wide coverage if requested. For failures, offer to hand off164 to `sf-debug` (log analysis) or `sf-apex` (fix the production code).165166Never run tests you just generated against a production org without telling167the user — test runs consume async capacity and coverage recalculation can be168slow in large orgs. Prefer sandboxes when the user has one connected.169170---171172## Cross-skill handoffs173174- Production code fixes or refactors → **sf-apex** (150-point rubric)175- Test failures needing log analysis → **sf-debug**176- Coverage strategy for an AppExchange submission → **sf-security**177 (review-readiness checklist includes the 75% bar and test quality)178179## References180181| File | Read when |182| --- | --- |183| `references/test-patterns.md` | Writing factories, mocks, async/event tests, runAs patterns |184| `references/execution-modes.md` | Start of session — detect mode |