
Generating Apex Test
- 1.6k installs
- 763 repo stars
- Updated July 24, 2026
- forcedotcom/afv-library
This is a copy of generating-apex-test by forcedotcom - installs and ranking accrue to the original listing.
generating-apex-test is a Claude Code skill that generates complete, Salesforce-compliant Apex test classes with bulk, positive, negative, and exception patterns for developers who must meet org coverage rules without wr
About
generating-apex-test is a Claude Code skill from forcedotcom/afv-library that scaffolds Apex test classes matching Salesforce deployment requirements. Generated tests include @TestSetup with TestDataFactory data, bulk operations using 251 or more records, positive and negative method paths, and exception-handling cases with Test.startTest and Test.stopTest boundaries. The template follows Given-When-Then structure and names methods like shouldPerformExpectedBehavior_WhenValidInput. Salesforce developers reach for generating-apex-test when adding coverage for new Apex classes, satisfying bulkification rules, or replacing hand-written test boilerplate before CI validation or org promotion.
- Generates @isTest classes with proper @TestSetup using TestDataFactory
- Includes positive, negative, and bulk (251+ records) test methods
- Enforces Given-When-Then structure with explicit Assert statements
- Provides exception handling and edge-case test templates
- Outputs ready-to-run test class following Salesforce best practices
Generating Apex Test by the numbers
- 1,593 all-time installs (skills.sh)
- +2 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/forcedotcom/afv-library --skill generating-apex-testAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 763 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 24, 2026 |
| Repository | forcedotcom/afv-library ↗ |
How do you write Salesforce Apex bulk test classes?
Generate complete, Salesforce-compliant Apex test classes that follow bulk, positive, negative, and exception patterns without manual boilerplate.
Who is it for?
Salesforce developers who need deployment-ready Apex test classes that satisfy bulkification and governor-limit best practices.
Skip if: Non-Salesforce Java or Node backends, or teams that only need manual exploratory testing without automated Apex coverage.
When should I use this skill?
The user asks to generate Apex tests, bulk test coverage, or Salesforce-compliant test classes for a specific Apex class.
What you get
Complete @isTest Apex classes with TestSetup, 251-record bulk cases, positive/negative tests, and exception coverage.
- Complete Apex test class source
- Bulk and exception test method scaffolds
By the numbers
- Scaffolds bulk Apex tests with 251 or more records per test class
Files
Generating Apex Tests
Generate production-ready Apex test classes and run disciplined test-fix loops with coverage analysis.
Core Principles
1. 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 2. 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 3. 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 4. 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 5. Use `Assert` class only — Assert.areEqual, Assert.isTrue, Assert.fail, etc. Never use legacy System.assert, System.assertEquals, or System.assertNotEquals 6. 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 7. Test negative paths — validate error handling and exception scenarios, not just happy paths 8. 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: 1. {ClassName}Test.cls — the test class (use assets/test-class-template.cls as starting point) 2. {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
Step 5 — Fix Loop
When tests fail, run a disciplined fix loop (max 3 iterations — stop and surface root cause if still failing):
1. Read the failing test class and the class under test 2. Identify root cause from error messages and stack traces 3. Apply fix — adjust test data or assertions for test-side issues; delegate production code issues to the generating-apex skill 4. Rerun the focused test before broader regression 5. 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; default66.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 |
/**
* Test class for {ClassUnderTest}.
* Tests bulk operations (251+ records), positive/negative paths,
* and exception handling.
*/
@isTest
private class {ClassUnderTest}Test {
@TestSetup
static void setupTestData() {
List<Account> accounts = TestDataFactory.createAccounts(251, true);
}
// ─── Positive Tests ───────────────────────────────────────────────────
@isTest
static void shouldPerformExpectedBehavior_WhenValidInput() {
// Given
List<Account> accounts = [SELECT Id, Name FROM Account];
// When
Test.startTest();
// {ClassUnderTest}.methodUnderTest(params);
Test.stopTest();
// Then
// Assert.areEqual(expected, actual, 'Descriptive failure message');
}
@isTest
static void shouldHandleBulkRecords_WhenProcessing251() {
// Given
List<Account> accounts = [SELECT Id FROM Account];
Assert.areEqual(251, accounts.size(), 'Should have 251 test records');
// When
Test.startTest();
// {ClassUnderTest}.bulkMethod(accounts);
Test.stopTest();
// Then
// List<Account> results = [SELECT Id, Status__c FROM Account];
// for (Account acc : results) {
// Assert.areEqual('Processed', acc.Status__c, 'All records should be processed');
// }
}
// ─── Negative Tests ───────────────────────────────────────────────────
@isTest
static void shouldThrowException_WhenNullInput() {
Test.startTest();
try {
// {ClassUnderTest}.methodUnderTest(null);
Assert.fail('Expected exception for null input');
} catch (MyCustomException e) {
Assert.isTrue(e.getMessage().contains('cannot be null'),
'Exception message should mention null input');
}
Test.stopTest();
}
@isTest
static void shouldReturnEmpty_WhenEmptyInput() {
Test.startTest();
// List<SObject> results = {ClassUnderTest}.methodUnderTest(new List<Id>());
Test.stopTest();
// Assert.isTrue(results.isEmpty(), 'Should return empty list for empty input');
}
// ─── Edge Case Tests ──────────────────────────────────────────────────
@isTest
static void shouldHandleMixedRecords_WhenSomeQualify() {
// Given
List<Account> accounts = [SELECT Id, Status__c FROM Account];
Integer half = accounts.size() / 2;
// for (Integer i = 0; i < half; i++) {
// accounts[i].Status__c = 'Qualifying';
// }
// update accounts;
// When
Test.startTest();
// {ClassUnderTest}.conditionalMethod(accounts);
Test.stopTest();
// Then
// List<Account> qualifying = [SELECT Id FROM Account WHERE Processed__c = true];
// Assert.areEqual(half, qualifying.size(), 'Only qualifying records should be processed');
}
}
/**
* @description Centralized factory for creating test data with sensible defaults.
* All methods accept a doInsert flag for flexibility.
* Bulk methods create multiple records; single-record methods delegate to bulk.
*/
@isTest
public class TestDataFactory {
// ─── Accounts ─────────────────────────────────────────────────────────
public static List<Account> createAccounts(Integer count, Boolean doInsert) {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < count; i++) {
accounts.add(new Account(
Name = 'Test Account ' + i,
BillingCity = 'San Francisco',
BillingState = 'CA',
BillingCountry = 'USA',
Industry = 'Technology',
Type = 'Customer'
));
}
if (doInsert) insert accounts;
return accounts;
}
public static Account createAccount(Boolean doInsert) {
return createAccounts(1, doInsert)[0];
}
// ─── Contacts ─────────────────────────────────────────────────────────
public static List<Contact> createContacts(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
List<Contact> contacts = new List<Contact>();
Integer idx = 0;
for (Account acc : accounts) {
for (Integer i = 0; i < countPerAccount; i++) {
contacts.add(new Contact(
FirstName = 'Test',
LastName = 'Contact ' + idx,
Email = 'test.contact' + idx + '@example.com',
AccountId = acc.Id
));
idx++;
}
}
if (doInsert) insert contacts;
return contacts;
}
// ─── Opportunities ────────────────────────────────────────────────────
public static List<Opportunity> createOpportunities(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
List<Opportunity> opps = new List<Opportunity>();
Integer idx = 0;
for (Account acc : accounts) {
for (Integer i = 0; i < countPerAccount; i++) {
opps.add(new Opportunity(
Name = 'Test Opportunity ' + idx,
AccountId = acc.Id,
StageName = 'Prospecting',
CloseDate = Date.today().addDays(30),
Amount = 10000 + (idx * 1000)
));
idx++;
}
}
if (doInsert) insert opps;
return opps;
}
// ─── Users ────────────────────────────────────────────────────────────
public static User createUser(String profileName, Boolean doInsert) {
Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
String uniqueKey = String.valueOf(DateTime.now().getTime());
User u = new User(
FirstName = 'Test',
LastName = 'User ' + uniqueKey,
Email = 'testuser' + uniqueKey + '@example.com',
Username = 'testuser' + uniqueKey + '@example.com.test',
Alias = 'tuser',
TimeZoneSidKey = 'America/Los_Angeles',
LocaleSidKey = 'en_US',
EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'en_US',
ProfileId = p.Id
);
if (doInsert) insert u;
return u;
}
// ─── Field Override Pattern ────────────────────────────────────────────
public static Account createAccount(Map<String, Object> fieldOverrides, Boolean doInsert) {
Account acc = new Account(
Name = 'Test Account',
Industry = 'Technology'
);
for (String fieldName : fieldOverrides.keySet()) {
acc.put(fieldName, fieldOverrides.get(fieldName));
}
if (doInsert) insert acc;
return acc;
}
// ─── Custom Objects ───────────────────────────────────────────────────
// Add methods for your custom objects following the same pattern:
// public static List<MyObject__c> createMyObjects(Integer count, Boolean doInsert) { ... }
}
Credits & Acknowledgments
This skill is built on established Salesforce Apex development patterns and the collective knowledge of the Salesforce developer community.
---
Assertion Patterns
Assertion Methods
| Method | Use Case |
|---|---|
Assert.areEqual(expected, actual, msg) | Exact equality |
Assert.areNotEqual(expected, actual, msg) | Value should differ |
Assert.isTrue(condition, msg) | Boolean condition |
Assert.isFalse(condition, msg) | Negated boolean condition |
Assert.fail(msg) | Force failure (e.g., expected exception not thrown) |
Assert.isNotNull(value, msg) | Non-null check |
Assert.isNull(value, msg) | Null check |
Always include the message parameter — makes test failures actionable.
Good vs Bad Assertions
Bad: No message, tests coverage not behavior
Assert.isTrue(result); // no message
Assert.isTrue(accounts.size() > 0); // vague — use areEqual with exact countGood: Descriptive message, tests specific behavior
Assert.isTrue(result, 'Service should return true for valid input');
Assert.areEqual(200, accounts.size(), 'All 200 accounts should be processed');Common Assertion Patterns
Collection Size
Assert.areEqual(200, results.size(), 'Should process all 200 records');
Assert.isTrue(results.isEmpty(), 'No results expected for invalid input');
Assert.isFalse(results.isEmpty(), 'Results should not be empty');Field Values
Assert.areEqual('Processed', acc.Status__c, 'Account status should be updated to Processed');
for (Account acc : updatedAccounts) {
Assert.areEqual('Active', acc.Status__c,
'Account ' + acc.Name + ' should have Active status');
}Exception Testing
@isTest
static void shouldThrowException_WhenInputInvalid() {
Test.startTest();
try {
MyService.process(null);
Assert.fail('Expected MyCustomException for null input');
} catch (MyCustomException e) {
Assert.isTrue(e.getMessage().contains('cannot be null'),
'Exception message should mention null input');
}
Test.stopTest();
}DML Results
// Insert success
Database.SaveResult[] results = Database.insert(accounts, false);
for (Database.SaveResult sr : results) {
Assert.isTrue(sr.isSuccess(), 'Insert should succeed: ' + sr.getErrors());
}
Database.SaveResult sr = Database.insert(invalidAccount, false);
Assert.isFalse(sr.isSuccess(), 'Insert should fail for invalid data');
Assert.isTrue(sr.getErrors()[0].getMessage().contains('REQUIRED_FIELD_MISSING'),
'Error should indicate missing required field');Null Checks
Assert.isNull(result.ErrorMessage__c, 'No error expected for valid input');
Assert.isNotNull(result.Id, 'Record should have been inserted');Date/DateTime
Assert.areEqual(Date.today(), record.CreatedDate__c, 'Should be created today');
Assert.isTrue(record.DueDate__c >= Date.today(), 'Due date should be in the future');Anti-Patterns
| Anti-Pattern | Fix |
|---|---|
Assert.isTrue(results.size() > 0) | Use Assert.areEqual(expectedCount, results.size(), ...) |
Assert.isTrue(results.size() >= expected) | Compute exact expected count, use Assert.areEqual |
| Testing implementation not behavior | Assert on observable outcomes (field values, record counts) |
| Missing negative test assertions | Verify the actual outcome, not just that no exception occurred |
Assert.isTrue(count != 0) | Use Assert.areEqual with deterministic value from test data |
Async Testing Patterns
Key Principle
Test.stopTest() forces all async operations to execute synchronously, allowing assertions on their results.
Batch Apex Testing
Basic Batch Test
Critical: In test context only one execute() invocation runs, so always set batchSize >= testRecordCount (e.g., Database.executeBatch(batch, 200) with 200 records). Never create more records than the batch size.
@isTest
static void shouldProcessAllRecords_WhenBatchExecutes() {
List<Account> accounts = TestDataFactory.createAccounts(200, true);
Test.startTest();
MyBatchClass batch = new MyBatchClass();
Id batchId = Database.executeBatch(batch, 200);
Test.stopTest();
List<Account> updated = [SELECT Id, Status__c FROM Account];
for (Account acc : updated) {
Assert.areEqual('Processed', acc.Status__c,
'Batch should update all account statuses');
}
}Batch with Failures
@isTest
static void shouldLogErrors_WhenRecordsFail() {
List<Account> accounts = TestDataFactory.createAccounts(198, true);
List<Account> invalidAccounts = new List<Account>();
for (Integer i = 0; i < 2; i++) {
invalidAccounts.add(new Account(
Name = 'Invalid Account ' + i,
Invalid_Field__c = 'triggers_validation_error'
));
}
insert invalidAccounts;
Test.startTest();
MyBatchClass batch = new MyBatchClass();
Database.executeBatch(batch, 200);
Test.stopTest();
List<Error_Log__c> errors = [SELECT Id, Message__c FROM Error_Log__c];
Assert.areEqual(2, errors.size(), 'Should log 2 failed records');
}Batch Chaining
Test finish() chaining in a separate test method — calling Database.executeBatch() inside finish() during a test can throw UnexpectedException. Verify the first batch independently, then test that finish() enqueues the next batch.
Queueable Testing
Basic Queueable Test
@isTest
static void shouldCompleteProcessing_WhenQueueableEnqueued() {
Account acc = TestDataFactory.createAccount(true);
Test.startTest();
MyQueueableClass queueable = new MyQueueableClass(acc.Id);
System.enqueueJob(queueable);
Test.stopTest();
Account updated = [SELECT Id, Status__c FROM Account WHERE Id = :acc.Id];
Assert.areEqual('Processed', updated.Status__c,
'Queueable should update account status');
}Queueable Chaining
Only the first chained queueable executes in tests. Design tests to verify: 1. First job completes correctly 2. Chain is properly enqueued (query AsyncApexJob) 3. Each job works independently in its own test method
@isTest
static void shouldChainNextJob_WhenMoreRecordsExist() {
List<Account> accounts = TestDataFactory.createAccounts(500, true);
Test.startTest();
MyChainedQueueable queueable = new MyChainedQueueable(0, 100);
System.enqueueJob(queueable);
Test.stopTest();
List<Account> processed = [SELECT Id FROM Account WHERE Processed__c = true];
Assert.areEqual(100, processed.size(), 'First batch should process 100 records');
List<AsyncApexJob> jobs = [
SELECT Id, Status, JobType
FROM AsyncApexJob
WHERE ApexClass.Name = 'MyChainedQueueable'
];
Assert.isTrue(jobs.size() >= 1, 'Chained job should be enqueued');
}Queueable with Callouts
Set the callout mock before Test.startTest():
@isTest
static void shouldMakeCallout_WhenQueueableWithCallout() {
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, '{"status":"ok"}'));
Account acc = TestDataFactory.createAccount(true);
Test.startTest();
MyQueueableWithCallout queueable = new MyQueueableWithCallout(acc.Id);
System.enqueueJob(queueable);
Test.stopTest();
Account updated = [SELECT Id, External_Status__c FROM Account WHERE Id = :acc.Id];
Assert.areEqual('Synced', updated.External_Status__c,
'Should update status after successful callout');
}Future Method Testing
@isTest
static void shouldExecuteFutureMethod() {
Account acc = TestDataFactory.createAccount(true);
Test.startTest();
MyClass.processFuture(acc.Id);
Test.stopTest();
Account updated = [SELECT Id, Processed__c FROM Account WHERE Id = :acc.Id];
Assert.isTrue(updated.Processed__c, 'Future should process record');
}Scheduled Apex Testing
Direct Execution
@isTest
static void shouldExecuteScheduledJob() {
List<Account> accounts = TestDataFactory.createAccounts(50, true);
Test.startTest();
MyScheduledClass scheduled = new MyScheduledClass();
scheduled.execute(null);
Test.stopTest();
List<Account> processed = [SELECT Id FROM Account WHERE Processed__c = true];
Assert.areEqual(50, processed.size(), 'Scheduled job should process records');
}CRON Registration
@isTest
static void shouldScheduleJob() {
Test.startTest();
String cronExp = '0 0 6 * * ?';
String jobId = System.schedule('Daily Processing', cronExp, new MyScheduledClass());
Test.stopTest();
CronTrigger ct = [
SELECT Id, CronExpression, State
FROM CronTrigger
WHERE Id = :jobId
];
Assert.areEqual('0 0 6 * * ?', ct.CronExpression, 'CRON should match');
Assert.areEqual('WAITING', ct.State, 'Job should be waiting');
}Common Pitfalls
| Pitfall | Impact |
|---|---|
Missing Test.stopTest() | Async never executes, assertions fail silently |
| Expecting all chained queueables to run | Only the first runs; test each independently |
Mock set after Test.startTest() | Callout mock must be set before Test.startTest() |
| Batch size < record count in tests | Only batchSize records processed; set batchSize >= recordCount |
Mocking Patterns
HTTP Callout Mocking
Apex doesn't allow real HTTP callouts in tests. Use HttpCalloutMock interface.
Basic Mock
@isTest
public class MockHttpResponse implements HttpCalloutMock {
private Integer statusCode;
private String body;
public MockHttpResponse(Integer statusCode, String body) {
this.statusCode = statusCode;
this.body = body;
}
public HTTPResponse respond(HTTPRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(statusCode);
res.setBody(body);
res.setHeader('Content-Type', 'application/json');
return res;
}
}Using the Mock
@isTest
static void shouldProcessApiResponse_WhenCalloutSucceeds() {
String mockResponse = '{"status": "success", "data": [{"id": "123"}]}';
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, mockResponse));
Test.startTest();
List<ExternalRecord> results = MyIntegrationService.fetchRecords();
Test.stopTest();
Assert.areEqual(1, results.size(), 'Should parse one record from response');
Assert.areEqual('123', results[0].externalId, 'Should extract correct ID');
}
@isTest
static void shouldHandleError_WhenCalloutFails() {
String errorResponse = '{"error": "Unauthorized"}';
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(401, errorResponse));
Test.startTest();
CalloutResult result = MyIntegrationService.fetchRecords();
Test.stopTest();
Assert.areEqual(false, result.isSuccess, 'Should indicate failure');
Assert.isTrue(result.errorMessage.contains('Unauthorized'), 'Should capture error');
}Multi-Request Mock
For services making multiple callouts:
@isTest
public class MultiRequestMock implements HttpCalloutMock {
private Map<String, HttpResponse> endpointResponses;
public MultiRequestMock(Map<String, HttpResponse> responses) {
this.endpointResponses = responses;
}
public HTTPResponse respond(HTTPRequest req) {
String endpoint = req.getEndpoint();
for (String key : endpointResponses.keySet()) {
if (endpoint.contains(key)) {
return endpointResponses.get(key);
}
}
HttpResponse res = new HttpResponse();
res.setStatusCode(404);
res.setBody('{"error": "Not found"}');
return res;
}
}StaticResourceCalloutMock
Use when response JSON is large or complex:
@isTest
static void shouldParseComplexResponse() {
StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
mock.setStaticResource('TestApiResponse');
mock.setStatusCode(200);
mock.setHeader('Content-Type', 'application/json');
Test.setMock(HttpCalloutMock.class, mock);
Test.startTest();
Result r = MyService.callExternalApi();
Test.stopTest();
Assert.isNotNull(r, 'Should parse response');
}SOSL Mocking
SOSL returns empty results in tests by default. Call Test.setFixedSearchResults(List<Id>) before the search:
@isTest
static void shouldReturnSearchResults() {
Account acc = TestDataFactory.createAccount(true);
Test.setFixedSearchResults(new List<Id>{ acc.Id });
Test.startTest();
List<Account> results = MyService.searchAccounts('Test');
Test.stopTest();
Assert.areEqual(1, results.size(), 'Should return mocked search result');
}DML Mocking (Constructor Injection)
Design for testability — use a public constructor for production and a @TestVisible private constructor that accepts mock interfaces:
public class MyService {
private IDML dmlHandler;
public MyService() {
this.dmlHandler = new DMLHandler();
}
@TestVisible
private MyService(IDML dmlHandler) {
this.dmlHandler = dmlHandler;
}
public void createRecords(List<Account> accounts) {
dmlHandler.doInsert(accounts);
}
}Stub API (System.StubProvider)
For mocking Apex class dependencies:
@isTest
public class MyServiceMock implements System.StubProvider {
public Object handleMethodCall(
Object stubbedObject, String stubbedMethodName, Type returnType,
List<Type> paramTypes, List<String> paramNames, List<Object> args
) {
if (stubbedMethodName == 'getAccountData') {
return new AccountData('Mock Account', 'Active');
}
return null;
}
}
@isTest
static void shouldUseAccountData() {
MyServiceMock mockProvider = new MyServiceMock();
IMyService mockService = (IMyService) Test.createStub(IMyService.class, mockProvider);
MyController controller = new MyController(mockService);
Test.startTest();
String result = controller.displayAccountInfo();
Test.stopTest();
Assert.isTrue(result.contains('Mock Account'), 'Should use mocked data');
}Email Testing
Apex doesn't actually send emails in tests. Use limits to verify:
@isTest
static void shouldSendEmail_WhenTriggered() {
Integer emailsBefore = Limits.getEmailInvocations();
Test.startTest();
MyService.sendNotification(testContact);
Test.stopTest();
Assert.areEqual(emailsBefore + 1, Limits.getEmailInvocations(),
'One email should be sent');
}Platform Event Testing
@isTest
static void shouldPublishEvent_WhenRecordCreated() {
Test.startTest();
Test.enableChangeDataCapture();
Account acc = TestDataFactory.createAccount(true);
Test.getEventBus().deliver();
Test.stopTest();
// Query platform event trigger results
List<EventLog__c> logs = [SELECT Id FROM EventLog__c WHERE AccountId__c = :acc.Id];
Assert.areEqual(1, logs.size(), 'Event handler should create log record');
}TestDataFactory Patterns
For the base class template, see assets/test-data-factory-template.cls.
Design Rules
1. Always accept a `doInsert` flag — lets callers modify records before insert 2. Append loop index to all fields that participate in matching rules — prevents DUPLICATES_DETECTED errors from active Duplicate Rules 3. Single-record methods delegate to bulk — e.g., createAccount(doInsert) calls createAccounts(1, doInsert)[0] 4. Return created records — enables chaining and further manipulation 5. Set all required fields — include fields enforced by validation rules, not just schema-required fields
Field Override Pattern
Allow callers to override default values without creating new factory methods:
public static Account createAccount(Map<String, Object> fieldOverrides, Boolean doInsert) {
Account acc = new Account(
Name = 'Test Account',
Industry = 'Technology'
);
for (String fieldName : fieldOverrides.keySet()) {
acc.put(fieldName, fieldOverrides.get(fieldName));
}
if (doInsert) insert acc;
return acc;
}
// Usage:
Account acc = TestDataFactory.createAccount(new Map<String, Object>{
'Name' => 'Custom Name',
'Industry' => 'Healthcare'
}, true);Record Type Support
public static Account createAccountByRecordType(String recordTypeName, Boolean doInsert) {
Id recordTypeId = Schema.SObjectType.Account
.getRecordTypeInfosByDeveloperName()
.get(recordTypeName)
.getRecordTypeId();
Account acc = new Account(
Name = 'Test Account',
RecordTypeId = recordTypeId
);
if (doInsert) insert acc;
return acc;
}Handling Duplicate Rules
When unique field values alone are not sufficient, use Database.insert() with a DuplicateRuleHeader:
public static List<Account> createAccountsAllowDuplicates(Integer count, Boolean doInsert) {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < count; i++) {
accounts.add(new Account(
Name = 'Test Account ' + i,
Phone = '555-000-' + String.valueOf(i).leftPad(4, '0')
));
}
if (doInsert) {
Database.DMLOptions dml = new Database.DMLOptions();
dml.DuplicateRuleHeader.allowSave = true;
Database.insert(accounts, dml);
}
return accounts;
}Related skills
How it compares
Pick generating-apex-test over generic unit-test skills when output must follow Salesforce @isTest, bulk, and governor-limit conventions.
FAQ
What bulk record count does generating-apex-test use?
generating-apex-test scaffolds bulk tests with 251 or more records in @TestSetup via TestDataFactory, matching Salesforce bulkification guidance for governor-limit-safe Apex test classes.
Which test patterns does generating-apex-test include?
generating-apex-test generates positive tests, negative tests, and exception-handling cases inside @isTest classes, using Test.startTest and Test.stopTest and Given-When-Then method structure.
Is Generating Apex Test safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.