AgentStack
SKILL verified MIT Self-run

Sf Apex Testing

skill-jiten-singh-shahi-salesforce-claude-code-sf-apex-testing · by jiten-singh-shahi

>-

No reviews yet
0 installs
20 views
0.0% view→install

Install

$ agentstack add skill-jiten-singh-shahi-salesforce-claude-code-sf-apex-testing

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Sf Apex Testing? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Apex Testing

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/TESTINGSTANDARDS.md


When to Use

  • When writing test classes for new Apex code before deploying to production
  • When existing tests pass but only cover happy paths
  • When a deployment fails with "insufficient code coverage" errors
  • When building mock patterns for callouts or dependency injection

> Related: For the TDD workflow (red-green-refactor process), see sf-tdd-workflow.


@isTest Annotation

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


@TestSetup

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 opps = new List();
    for (Integer i = 0; i ());
    }

    public static Account createAccount(Map 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 createAccounts(Integer count) {
        List accounts = new List();
        for (Integer i = 0; i  overrides) {
        for (String fieldName : overrides.keySet()) {
            record.put(fieldName, overrides.get(fieldName));
        }
    }
}

Test Structure: Arrange-Act-Assert

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{ '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');
}

Test Method Naming

Format: test{MethodName}_{scenario}_{expectedResult}

testCalculateDiscount_premiumTier_returns20Percent()
testCalculateDiscount_nullAmount_returnsZero()
testCreateAccount_duplicateName_addsFieldError()

Governor Limit Testing

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 accounts = TestDataFactory.createAccounts(200);

    List processed = [
        SELECT Id, Customer_Tier__c FROM Account
        WHERE Id IN :new Map(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.


Exception Testing

@isTest
static void testUpgradeToPremium_insufficientRevenue_throwsUpgradeException() {
    Account acc = TestDataFactory.createAccount(
        new Map{ 'AnnualRevenue' => 10000 }
    );

    Test.startTest();
    try {
        AccountsService.upgradeToPremium(new Set{ 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/APIVERSIONS.md for minimum version): Assert.areEqual, Assert.isTrue, Assert.isNotNull, Assert.fail.


Permission Testing with System.runAs

@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();
}

Async Testing

@future and Queueable

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 errors = [
        SELECT Id FROM Integration_Error_Log__c WHERE Account__c = :acc.Id
    ];
    System.assertEquals(0, errors.size(), 'No errors should be logged');
}

Batch Apex

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

Mock Patterns

HttpCalloutMock

@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;
    }
}

Multi-Callout Mock

@isTest
public class MultiCalloutMock implements HttpCalloutMock {
    private Map responses = new Map();

    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());
    }
}

Stub API (System.StubProvider)

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.


Coverage Strategy

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;
}

RunRelevantTests and @testFor (Spring '26)

> Requires the minimum API version for this feature (see @../reference/APIVERSIONS.md). If sfdx-project.json specifies a sourceApiVersion below it, RunRelevantTests silently falls back to RunLocalTests.

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


Related

  • Agent: sf-apex-agent — For interactive, in-depth guidance
  • Skills: sf-tdd-workflow — TDD workflow for Apex

Guardrails

  • sf-testing-constraints — Enforces test isolation, assertion requirements, SeeAllData prohibition, and coverage thresholds

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.