---
title: "Mocking HTTP callouts with HttpCalloutMock"
url: "https://trailassembly.com/httpcalloutmock-testing/"
topic: "Apex and Development"
published: "2026-09-11T12:32:15+00:00"
updated: "2026-10-06T13:53:05+00:00"
error_codes: "CalloutException"
---

# Mocking HTTP callouts with HttpCalloutMock

**The answer, briefly:** Apex tests cannot make HTTP callouts, so you register a mock that returns a response you construct. `Test.setMock(HttpCalloutMock.class, new YourMock())` before `Test.startTest()`, and every callout in the test gets your response instead of the network. The part people get wrong is testing only the happy path.

## The minimum that works

```apex
@IsTest
private class ErpClientTest {

    private class OkMock implements HttpCalloutMock {
        public HttpResponse respond(HttpRequest req) {
            HttpResponse res = new HttpResponse();
            res.setStatusCode(200);
            res.setHeader('Content-Type', 'application/json');
            res.setBody('{"orderId":"ERP-1001","status":"accepted"}');
            return res;
        }
    }

    @IsTest
    static void acceptsOrder() {
        Test.setMock(HttpCalloutMock.class, new OkMock());
        Test.startTest();
        ErpClient.Result r = ErpClient.send(new Order(ref = 'A1'));
        Test.stopTest();
        System.assertEquals('ERP-1001', r.externalId, 'external id should be read from the body');
    }
}
```

Two rules the compiler will not enforce. The mock must be registered before the callout happens, which in practice means before `Test.startTest()`. And a callout made from a [queueable](https://trailassembly.com/apex-choosing-async/) or future method only runs at `Test.stopTest()`, so the mock has to be registered before that too.

## Assert on the request, not just the response

The mock receives the `HttpRequest`. Most tests throw it away, which means they never verify that you sent the right thing. That is half the value of the test:

```apex
public HttpResponse respond(HttpRequest req) {
    System.assertEquals('POST', req.getMethod());
    System.assert(req.getEndpoint().endsWith('/api/orders'),
        'endpoint was ' + req.getEndpoint());
    Map<String, Object> body =
        (Map<String, Object>) JSON.deserializeUntyped(req.getBody());
    System.assertEquals('A1', body.get('reference'));
    ...
}
```

An [assertion](https://trailassembly.com/apex-assert-exception-message/) inside the mock fails the test with a useful message, and it catches the class of bug where you refactor the payload and every test still passes because they only check the response you hard-coded.

## The cases that actually break in production

A single 200 mock tests almost nothing. Write one test per failure mode, because each has a different correct behaviour:

- **500 from the server.** Should retry, or record and move on. Should not throw an unhandled exception at a user.

- **401 or 403.** Should not retry — the credentials will not fix themselves on a timer.

- **A timeout.** Throw `new CalloutException('Read timed out')` from the mock. This is the one people skip and it is the one that [happens most](https://trailassembly.com/callout-read-timed-out/), and a timeout does not mean nothing happened at the far end.

- **A 200 with a body you did not expect** — empty, HTML from a proxy, or valid JSON with a missing field. Real integrations receive all three.

```apex
private class TimeoutMock implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest req) {
        throw new CalloutException('Read timed out');
    }
}
```

## Several callouts in one test

A single mock class handles this by branching on the endpoint, which keeps the routing visible:

```apex
public HttpResponse respond(HttpRequest req) {
    if (req.getEndpoint().contains('/token')) { return tokenResponse(); }
    if (req.getEndpoint().contains('/orders')) { return orderResponse(); }
    throw new CalloutException('unmocked endpoint: ' + req.getEndpoint());
}
```

The final throw is the useful line. Without it, an unexpected callout gets a null response and fails somewhere confusing.

## Static resources, and why they age badly

`Test.setMock(HttpCalloutMock.class, new StaticResourceCalloutMock())` serves a response body from a static resource. It is convenient for a large payload and it puts the fixture somewhere nobody looks. A body that lives in the test class is read by whoever is debugging the test. A static resource is not.

## Frequently asked

**Can I test without a mock?** No. An unmocked callout in a test throws “You have [uncommitted work](https://trailassembly.com/uncommitted-work-pending/) pending” or a methods-defined-as-TestMethod error, depending on the path.

**Does the mock apply to [Named Credential](https://trailassembly.com/named-credentials-end-to-end/) callouts?** Yes. The endpoint arrives as the `callout:` string, so assert against that rather than a full URL.

**What about `WebServiceMock`?** Same idea for SOAP calls generated from WSDL. Different interface, same registration through `Test.setMock`.

---

Source: https://trailassembly.com/httpcalloutmock-testing/
Trail Assembly — independent Salesforce write-ups.
