Advanced Testing & Mocking

26.1 Mocking HTTP callouts

Tests cannot make real callouts. Implement HttpCalloutMock:

Alternatives: StaticResourceCalloutMock (response from a static resource), MultiStaticResourceCalloutMock (different resources per endpoint), and WebServiceMock for SOAP stubs generated by WSDL2Apex (Test.setMock(WebServiceMock.class, new MySoapMock())).

If a test performs DML and then a callout, you'll get *"You have uncommitted work pending"*. Do the DML before Test.startTest() and the callout between startTest and stopTest.

@isTest
public class WeatherCalloutMock implements HttpCalloutMock {
    private Integer statusCode;
    private String body;
    public WeatherCalloutMock(Integer statusCode, String body) {
        this.statusCode = statusCode; this.body = body;
    }
    public HttpResponse respond(HttpRequest req) {
        Assert.areEqual('GET', req.getMethod());
        HttpResponse res = new HttpResponse();
        res.setHeader('Content-Type', 'application/json');
        res.setStatusCode(statusCode);
        res.setBody(body);
        return res;
    }
}

@isTest
private class WeatherServiceTest {
    @isTest
    static void parsesTemperature() {
        Test.setMock(HttpCalloutMock.class, new WeatherCalloutMock(200, '{"tempC": 21.5}'));
        Test.startTest();
        Decimal t = WeatherService.currentTemp('Pune');
        Test.stopTest();
        Assert.areEqual(21.5, t);
    }

    @isTest
    static void handlesServerError() {
        Test.setMock(HttpCalloutMock.class, new WeatherCalloutMock(500, 'oops'));
        try {
            WeatherService.currentTemp('Pune');
            Assert.fail('Expected exception');
        } catch (WeatherService.WeatherException e) {
            Asse
…

26.2 The Stub API

System.StubProvider + Test.createStub() create mock implementations of non-static methods on your classes, without real database work:

Stub API limitations: the stub can only intercept non-static, non-private instance methods; you cannot stub static methods, constructors, private methods, properties' get/set accessors, system classes, or classes from another namespace; and the code under test must receive the stub (via constructor or setter injection, §36) instead of instantiating the real class itself.

Frameworks built on the Stub API: ApexMocks (fflib, Mockito-style when().thenReturn() and verify()), Amoss, UniversalMocker.

public virtual class AccountSelector {                          // must be stubbable: not final, visible
    public virtual List<Account> selectByIndustry(String industry) {
        return [SELECT Id, Name FROM Account WHERE Industry = :industry];
    }
}

@isTest
public class AccountSelectorStub implements System.StubProvider {
    public Object handleMethodCall(Object stubbedObject, String stubbedMethodName,
                                   Type returnType, List<Type> listOfParamTypes,
                                   List<String> listOfParamNames, List<Object> listOfArgs) {
        if (stubbedMethodName == 'selectByIndustry') {
            return new List<Account>{
                new Account(Id = fakeId(), Name = 'Mocked ' + listOfArgs[0])
            };
        }
        return null;
    }
    private static Id fakeId() {
        return Id.valueOf('001' + '0'.repeat(11) + '1');         // fake 15-char Account ID (never inserted)
    }
}

@isTest
static void serviceUsesSelector() {
    AccountSelector stub = (AccountSelector) Test.createStub(AccountSelector.class, new AccountSelectorStub());
    AccountService svc = new AccountService(stub);              // constructor inje
…

26.3 Testing async code

@future, Queueable Call inside Test.startTest()/stopTest(); runs at stopTest

Only the first job runs in tests; guard chaining with Test.isRunningTest() or test each job separately

Database.executeBatch inside start/stop; only one execute chunk runs in tests — keep test data ≤ scope size

System.schedule inside start/stop; execute runs at stopTest (but anything it enqueues does not)

Test.getEventBus().deliver() or stopTest Test the finalizer class directly by implementing a fake FinalizerContext, or rely on stopTest

26.4 Test quality checklist

Every test has at least one meaningful assertion with a message.

Positive, negative, bulk (≥ 200 records), and permission (runAs) cases.

No SeeAllData=true; no hard-coded IDs; no dependency on org configuration that may change.

Data created via a factory; @testSetup for shared fixtures.

Unit tests use mocks/stubs for selectors and callouts; a few integration tests hit the database.

Tests run fast and in parallel.