Guides Apex unit testing with @isTest, @TestSetup, TestDataFactory, governor limits, async testing, mocks, and coverage strategies. Use for writing tests or fixing coverage issues.
npx claudepluginhub jiten-singh-shahi/salesforce-claude-code --plugin salesforce-claude-codeThis skill is limited to using the following tools:
Procedures and patterns for writing effective Apex tests. Constraint rules (never/always lists for test isolation, assertions, SeeAllData) live in `sf-testing-constraints`. This skill covers the _how_ — test structure, factories, mocks, async testing, and coverage strategies.
Enforces Salesforce Apex quality guardrails: bulk-safety (no SOQL/DML in loops), sharing declarations, CRUD/FLS security, SOQL injection prevention, PNB test coverage. Use for reviewing or generating classes, triggers, batches, tests.
Provides patterns for Salesforce platform development: Lightning Web Components (LWC), Apex triggers/classes, REST/Bulk APIs, Connected Apps, Salesforce DX with scratch orgs and 2GP.
Set up Salesforce CI/CD pipelines with GitHub Actions, SFDX deployments, JWT auth, and Apex testing. For automating metadata validation and tests in Salesforce repos.
Share bugs, ideas, or general feedback.
Procedures and patterns for writing effective Apex tests. Constraint rules (never/always lists for test isolation, assertions, SeeAllData) live in sf-testing-constraints. This skill covers the how — test structure, factories, mocks, async testing, and coverage strategies.
Reference: @../_reference/TESTING_STANDARDS.md
Related: For the TDD workflow (red-green-refactor process), see
sf-tdd-workflow.
@isTest
private class AccountServiceTest {
@TestSetup
static void makeData() {
// Runs once before any test method in this class
// Each test method gets a fresh transaction with this data
}
@isTest
static void testCreateAccount_validData_createsSuccessfully() {
// Arrange / Act / Assert
}
}
Test classes do not count toward coverage calculations but DO count toward the 6 MB Apex code character limit. Use @TestVisible to make private members accessible in tests without changing access modifiers.
Runs once per test class. Each test method gets its own database rollback, so modifications in one test do not bleed into another.
@TestSetup
static void makeData() {
Account acc = new Account(
Name = 'Test Corp',
Type = 'Customer',
AnnualRevenue = 1000000,
Customer_Tier__c = 'Standard'
);
insert acc;
List<Opportunity> opps = new List<Opportunity>();
for (Integer i = 0; i < 10; i++) {
opps.add(new Opportunity(
Name = 'Test Opp ' + i,
AccountId = acc.Id,
StageName = i < 5 ? 'Prospecting' : 'Qualification',
CloseDate = Date.today().addDays(30 + i),
Amount = 5000 * (i + 1)
));
}
insert opps;
}
A central factory class creates test records consistently across the test suite, preventing duplicated record-creation logic.
@isTest
public class TestDataFactory {
public static Account createAccount() {
return createAccount(new Map<String, Object>());
}
public static Account createAccount(Map<String, Object> overrides) {
Account acc = new Account(
Name = 'Test Account ' + generateUniqueString(),
Type = 'Customer',
Industry = 'Technology',
AnnualRevenue = 500000,
Customer_Tier__c = 'Standard'
);
applyOverrides(acc, overrides);
insert acc;
return acc;
}
public static List<Account> createAccounts(Integer count) {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < count; i++) {
accounts.add(new Account(
Name = 'Bulk Test Account ' + i,
Type = 'Customer',
Customer_Tier__c = 'Standard'
));
}
insert accounts;
return accounts;
}
public static User createUserWithProfile(String profileName) {
Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
User u = new User(
Alias = 'tstuser',
Email = generateUniqueString() + '@testfactory.example.com',
EmailEncodingKey = 'UTF-8',
LastName = 'Testing',
LanguageLocaleKey = 'en_US',
LocaleSidKey = 'en_US',
ProfileId = p.Id,
TimeZoneSidKey = 'America/Los_Angeles',
UserName = generateUniqueString() + '@testfactory.example.com'
);
insert u;
return u;
}
private static Integer uniqueCounter = 0;
private static String generateUniqueString() {
return String.valueOf(++uniqueCounter) + '_' +
String.valueOf(Datetime.now().getTime()).right(6);
}
private static void applyOverrides(SObject record, Map<String, Object> overrides) {
for (String fieldName : overrides.keySet()) {
record.put(fieldName, overrides.get(fieldName));
}
}
}
Every test method follows three phases with a blank line between them.
@isTest
static void testCalculateDiscount_premiumTier_returns20Percent() {
// Arrange
Account acc = TestDataFactory.createAccount(
new Map<String, Object>{ 'Customer_Tier__c' => 'Premium' }
);
Decimal orderAmount = 10000;
// Act
Test.startTest();
Decimal discount = DiscountCalculator.calculate(acc.Id, orderAmount);
Test.stopTest();
// Assert
Assert.areEqual(2000, discount,
'Premium tier accounts should receive a 20% discount on $10,000 orders');
}
Format: test{MethodName}_{scenario}_{expectedResult}
testCalculateDiscount_premiumTier_returns20Percent()
testCalculateDiscount_nullAmount_returnsZero()
testCreateAccount_duplicateName_addsFieldError()
Always test with 200 records (standard trigger batch size). A method that works with 1 record may fail at governor limits with 200.
@isTest
static void testTrigger_bulkInsert_staysWithinLimits() {
List<Account> accounts = TestDataFactory.createAccounts(200);
List<Account> processed = [
SELECT Id, Customer_Tier__c FROM Account
WHERE Id IN :new Map<Id, Account>(accounts).keySet()
];
System.assertEquals(200, processed.size(), 'All 200 accounts should be present');
}
Use Test.startTest() / Test.stopTest() to reset governor limit counters, giving the code under test a fresh limit context.
@isTest
static void testUpgradeToPremium_insufficientRevenue_throwsUpgradeException() {
Account acc = TestDataFactory.createAccount(
new Map<String, Object>{ 'AnnualRevenue' => 10000 }
);
Test.startTest();
try {
AccountsService.upgradeToPremium(new Set<Id>{ acc.Id });
Assert.fail('Expected UpgradeException was not thrown');
} catch (AccountsService.UpgradeException e) {
Assert.isTrue(
e.getMessage().contains('Annual revenue must be at least'),
'Exception message should explain the reason. Got: ' + e.getMessage()
);
}
Test.stopTest();
}
Use the Assert class (see @../_reference/API_VERSIONS.md for minimum version): Assert.areEqual, Assert.isTrue, Assert.isNotNull, Assert.fail.
@isTest
static void testViewRestrictedReport_standardUser_throwsException() {
User standardUser = TestDataFactory.createUserWithProfile('Standard User');
Restricted_Report__c report = new Restricted_Report__c(Name = 'Confidential Q4');
insert report;
Test.startTest();
System.runAs(standardUser) {
try {
ReportService.viewReport(report.Id);
Assert.fail('Standard user should not be able to view restricted reports');
} catch (ReportService.AccessDeniedException e) {
Assert.isTrue(true, 'Expected AccessDeniedException thrown correctly');
}
}
Test.stopTest();
}
Test.startTest() / Test.stopTest() forces @future and Queueable jobs to execute synchronously.
@isTest
static void testFutureCallout_sendsRequest() {
Test.setMock(HttpCalloutMock.class, new MockERPCallout(200, '{"status":"ok"}'));
Account acc = TestDataFactory.createAccount();
Test.startTest();
ExternalDataSync.syncAccountToERP(acc.Id);
Test.stopTest();
List<Integration_Error_Log__c> errors = [
SELECT Id FROM Integration_Error_Log__c WHERE Account__c = :acc.Id
];
System.assertEquals(0, errors.size(), 'No errors should be logged');
}
@isTest
static void testBatch_processesAllRecords() {
// insert 200 records
Test.startTest();
Database.executeBatch(new AccountAnnualReviewBatch(), 200);
Test.stopTest(); // start(), execute(), finish() all run synchronously
// Assert results
}
@isTest
public class MockERPCallout implements HttpCalloutMock {
private Integer statusCode;
private String responseBody;
public MockERPCallout(Integer statusCode, String responseBody) {
this.statusCode = statusCode;
this.responseBody = responseBody;
}
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(statusCode);
res.setBody(responseBody);
res.setHeader('Content-Type', 'application/json');
return res;
}
}
@isTest
public class MultiCalloutMock implements HttpCalloutMock {
private Map<String, HttpResponse> responses = new Map<String, HttpResponse>();
public MultiCalloutMock addResponse(String urlPattern, Integer statusCode, String body) {
HttpResponse res = new HttpResponse();
res.setStatusCode(statusCode);
res.setBody(body);
responses.put(urlPattern, res);
return this;
}
public HttpResponse respond(HttpRequest req) {
for (String pattern : responses.keySet()) {
if (req.getEndpoint().contains(pattern)) {
return responses.get(pattern);
}
}
throw new CalloutException('No mock response configured for: ' + req.getEndpoint());
}
}
For mocking dependencies without HTTP, use the System.StubProvider interface with Test.createStub().
IAccountsSelector mockSelector = (IAccountsSelector)
Test.createStub(IAccountsSelector.class, new MockAccountsSelector(mockAccounts));
Note: The instance field must be typed as the interface (e.g.,
IAccountsSelector), not the concrete class. Casting a stub proxy to a concrete class throws TypeException at runtime.
Line coverage is misleading. A method with an if statement can show 100% line coverage if you only test the true branch. Test every branch.
// This method has 4 branches — test each:
// 1. testCalculateDiscount_premiumTier
// 2. testCalculateDiscount_standardTier
// 3. testCalculateDiscount_unknownTier (else)
// 4. testCalculateDiscount_nullTier
public Decimal calculateDiscount(String tier, Decimal amount) {
if (tier == 'Premium') return amount * 0.20;
else if (tier == 'Standard') return amount * 0.10;
else return 0;
}
Requires the minimum API version for this feature (see @../_reference/API_VERSIONS.md). If
sfdx-project.jsonspecifies asourceApiVersionbelow it,RunRelevantTestssilently falls back toRunLocalTests.
@testFor explicitly declares which production class a test class covers, improving RunRelevantTests selection accuracy.
@isTest
@testFor(AccountService)
private class AccountServiceTest {
// tests for AccountService
}
Rules: Reference any top-level Apex class (not inner classes). Cannot be placed on non-test classes. Use separate test classes for each target — Apex does not support stacking duplicate annotations.
sf-apex-agent — For interactive, in-depth guidancesf-tdd-workflow — TDD workflow for Apexsf-testing-constraints — Enforces test isolation, assertion requirements, SeeAllData prohibition, and coverage thresholds