Implement HttpCalloutMock — one method, respond(HttpRequest req) — and register it with Test.setMock(HttpCalloutMock.class, new MyMock()) before the callout runs. Every callout in that test then receives your response instead of hitting the network. Build a configurable mock for simple cases and a router mock for code that calls more than one endpoint. Once mocking is trivial, test the 500s and malformed bodies too — not just the 200.
Why tests can't make real callouts
Run a test that hits an external endpoint without mocking it and you get:
System.CalloutException: Methods defined as TestMethod do not support Web service calloutsThis is deliberate, not a gap. A deploy gate that depends on a third party being up, fast, and returning the same thing every time isn't a gate — it's a coin flip with extra steps. Salesforce blocks the real network call unconditionally in test context and gives you a seam to fill instead: HttpCalloutMock.
The interface: one method
HttpCalloutMock has exactly one method to implement — respond, which takes the outgoing HttpRequest and returns the HttpResponse your code will receive. Register an instance with Test.setMock before the code under test runs; from that point, every Http.send() call in the test is routed to your mock instead of the network.
A minimal example — a service that calls an endpoint and parses JSON back:
public class PaymentGatewayService {
public class PaymentResult {
public String status;
public String transactionId;
}
public static PaymentResult charge(Decimal amount) {
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.paymentgateway.example.com/v1/charges');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(new Map<String, Object>{ 'amount' => amount }));
HttpResponse res = new Http().send(req);
if (res.getStatusCode() != 200) {
throw new CalloutException('Charge failed: ' + res.getStatusCode());
}
return (PaymentResult) JSON.deserialize(res.getBody(), PaymentResult.class);
}
}@isTest
private class PaymentGatewayServiceTest {
private class SuccessMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setHeader('Content-Type', 'application/json');
res.setBody('{"status":"approved","transactionId":"txn_8842"}');
return res;
}
}
@isTest
static void chargeReturnsApprovedResult() {
Test.setMock(HttpCalloutMock.class, new SuccessMock());
Test.startTest();
PaymentGatewayService.PaymentResult result = PaymentGatewayService.charge(49.99);
Test.stopTest();
System.assertEquals('approved', result.status);
System.assertEquals('txn_8842', result.transactionId);
}
}No network call happened. The test asserts on your parsing logic, not on a third party's uptime.
Pattern 2: a configurable mock for the whole suite
Writing a new inner class per scenario gets old fast. A single mock whose constructor takes the status code and body covers most of a test class:
@isTest
public class HttpMock implements HttpCalloutMock {
private Integer statusCode;
private String body;
private Map<String, String> headers;
public HttpMock(Integer statusCode, String body) {
this(statusCode, body, new Map<String, String>{ 'Content-Type' => 'application/json' });
}
public HttpMock(Integer statusCode, String body, Map<String, String> headers) {
this.statusCode = statusCode;
this.body = body;
this.headers = headers;
}
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(statusCode);
for (String key : headers.keySet()) {
res.setHeader(key, headers.get(key));
}
res.setBody(body);
return res;
}
}Test.setMock(HttpCalloutMock.class,
new HttpMock(200, '{"status":"approved","transactionId":"txn_8842"}'));One reusable class, one line per test to describe the response you want. This is usually the only mock a test class needs until code starts making more than one callout per transaction.
Pattern 3: a router mock for multi-callout flows
Auth-then-fetch is the common case that breaks a single canned response: the code calls a token endpoint, then calls the data endpoint with that token. One respond implementation can't return two different bodies unless it looks at the request first — so switch on the endpoint (and method, if needed):
@isTest
public class HttpRouterMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
if (req.getEndpoint().contains('/oauth/token')) {
res.setStatusCode(200);
res.setBody('{"access_token":"tok_abc123","expires_in":3600}');
} else if (req.getEndpoint().contains('/v1/accounts') && req.getMethod() == 'GET') {
res.setStatusCode(200);
res.setBody('{"accounts":[{"id":"acc_1","balance":500.00}]}');
} else {
res.setStatusCode(404);
res.setBody('{"error":"unmapped endpoint in test mock"}');
}
return res;
}
}The else branch matters as much as the matched ones — it turns a missing case in the router into a loud 404 in the test output instead of a silent wrong response.
The actual payoff: testing what breaks
A mock that only ever returns 200 tests the same one path a manual run through the happy case already covers. The value of mocking is that failure responses cost nothing extra to construct — the branches nobody tests when every attempt means a real callout and a wait:
private class ServerErrorMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(500);
res.setBody('{"error":"internal_server_error"}');
return res;
}
}
private class MalformedBodyMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setBody('{not valid json');
return res;
}
}
private class TimeoutMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
throw new CalloutException('Read timed out');
}
}A mock's respond method can throw, which is how a timeout is represented — the real Http.send() throws CalloutException when a request times out, so a mock that does the same exercises your catch block honestly. Three mocks, three tests, and now the retry logic, the error-surface-to-user path, and the parse failure handling all have coverage instead of a comment that says "should probably handle this."
Gotcha: "You have uncommitted work pending"
A callout issued after DML in the same transaction throws:
System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling outThis is a real platform rule, not a test artifact — Salesforce won't let a transaction hold an open database write while it waits on a network round trip. In tests it typically shows up because @testSetup or arrange-phase DML runs before the callout in the same transaction. The standard fix is putting the callout inside Test.startTest() / Test.stopTest() — that boundary starts a fresh execution context with its own limits, separate from whatever DML ran to set up the test data. In real (non-test) code, the equivalent fix is structural: order callouts before DML, or move the DML to run asynchronously after the callout completes.
Two related tools worth knowing about: StaticResourceCalloutMock and MultiStaticResourceCalloutMock serve response bodies straight from a static resource instead of an inline string, which is convenient for large or shared fixture payloads. And if the integration under test is SOAP rather than REST — a WSDL-generated class — the equivalent interface is WebServiceMock, registered the same way through Test.setMock.
Running this locally with Nimbus
Everything above is standard Apex — HttpCalloutMock, Test.setMock, StaticResourceCalloutMock, WebServiceMock — and none of it needs a live org to exercise, because the whole point of mocking is that the network never gets touched. Nimbus runs that same code locally, against an embedded PostgreSQL, with no org and no deploy step:
nimbus test "PaymentGatewayServiceTest.*"The loop this shortens is the one where you're iterating on the mock and the parser together — tweak the mock's JSON body, adjust the deserialization, rerun, repeat. On an org that's a deploy and a wait measured in minutes per attempt. Locally it's sub-second, which is what makes writing the full 500 / timeout / malformed-body matrix for a callout worth doing instead of skipping in favor of "we'll catch it in QA."
Checklist
- No mock, no test. A real callout in test context throws immediately — this is enforced, not optional.
- One method.
HttpCalloutMock.respond(HttpRequest req)is the entire interface. - Configurable mock for the suite, router mock for multi-callout flows. Switch on
req.getEndpoint()when code calls more than one URL. - Test the failure responses, not just the 200 — 500s, malformed bodies, and thrown
CalloutExceptionfor timeouts. - Callout after DML throws. Put the callout inside
Test.startTest()/Test.stopTest(), or reorder real code so callouts precede DML.
Run the mock-and-parse loop locally
Same HttpCalloutMock code, no org, no deploy — iterate on the mock and the parser in the same second.