# Sf Apex Testing

> >-

- **Type:** Skill
- **Install:** `agentstack add skill-jiten-singh-shahi-salesforce-claude-code-sf-apex-testing`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [jiten-singh-shahi](https://agentstack.voostack.com/s/jiten-singh-shahi)
- **Installs:** 0
- **Category:** [Cloud & Infrastructure](https://agentstack.voostack.com/c/cloud-infrastructure)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [jiten-singh-shahi](https://github.com/jiten-singh-shahi)
- **Source:** https://github.com/jiten-singh-shahi/salesforce-claude-code/tree/main/.cursor/skills/sf-apex-testing
- **Website:** https://www.npmjs.com/package/scc-universal

## Install

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

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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/TESTING_STANDARDS.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

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

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

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

```apex
@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

```apex
@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/API_VERSIONS.md for minimum version): `Assert.areEqual`, `Assert.isTrue`, `Assert.isNotNull`, `Assert.fail`.

---

## Permission Testing with System.runAs

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

```apex
@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

```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

```apex
@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

```apex
@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()`.

```apex
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.

```apex
// 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/API_VERSIONS.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.

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

- **Author:** [jiten-singh-shahi](https://github.com/jiten-singh-shahi)
- **Source:** [jiten-singh-shahi/salesforce-claude-code](https://github.com/jiten-singh-shahi/salesforce-claude-code)
- **License:** MIT
- **Homepage:** https://www.npmjs.com/package/scc-universal

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

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-jiten-singh-shahi-salesforce-claude-code-sf-apex-testing
- Seller: https://agentstack.voostack.com/s/jiten-singh-shahi
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
