Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

The short version: Apex mocks prove your code handles a response correctly, sandbox runs against a real endpoint prove the connection works, and contract tests prove the other side has not changed its mind. They catch three different classes of failure and none of them substitutes for the others. A team with only mocks has 90% coverage and an integration that breaks in production the first time the partner adds a required field.

What mocks actually prove

Apex cannot make HTTP callouts in a test, so you register a mock and every callout in that test gets your canned response. That is genuinely useful, and it is useful for a narrower reason than most people think: it tests your parsing and your error handling, not the integration.

Test.setMock(HttpCalloutMock.class, new ErpMock(500, '{"error":"busy"}'));
Test.startTest();
new ErpSync().push(accounts);
Test.stopTest();

The valuable tests here are the ugly ones. A 500. A timeout, which you simulate by throwing CalloutException from the mock. Malformed JSON. A 200 with an empty body. A field that arrives null when your code assumes a value. Write those before you write the happy path, because the happy path is the one case you will definitely notice by hand.

Assert on the request too, not only on what your code did with the response. The mock receives the HttpRequest, so check the method, the endpoint, the headers and the serialised body. A test that never looks at what you sent will happily pass while you send the wrong payload.

For SOAP callouts the equivalent is WebServiceMock, which works on the same principle against generated stub classes.

What only a real endpoint catches

Everything environmental. Named credential configuration. Certificate chains and TLS versions. The partner’s IP allowlist not including your sandbox. Authentication that works with a hand-made token and fails with the one your JWT flow produces. Response times that are fine at ten records and unacceptable at two hundred. Payload shapes that differ from the documentation in small, infuriating ways.

Run this in a sandbox against the partner’s test environment, on a schedule, not just once during the build. A connection that worked in March and was never exercised again is not a working connection. A nightly smoke test that does one real round trip and alerts on failure is the cheapest insurance available.

The cost is real: partner test environments need credentials, data, and a way to clean up after yourself. Budget for it in the project rather than discovering it in week nine.

Contract tests

The third layer sits between the two. Take the provider’s OpenAPI document or WSDL, and test your serialisation and deserialisation against it in CI. Not against a live endpoint, against the spec file.

This catches drift. When the provider adds a required field, changes a type from string to integer, or renames something in a minor release, the contract test fails in your build rather than in production at two in the morning. It also catches your own drift in the other direction, when someone adds a field to the outbound payload that the provider will reject.

Keep the spec file under version control and update it deliberately. A diff on that file during a dependency bump is one of the more informative code reviews you will do.

The layers, and what each finds

  • Apex mocks. Parsing bugs, unhandled error paths, governor limits under realistic payload sizes, bulk behaviour at 200 records.
  • Contract tests. Schema drift on either side, type mismatches, newly required fields.
  • Sandbox with real endpoints. Auth, network, certificates, timeouts, and the gap between documentation and reality.
  • Production monitoring. Everything else, because something always gets through.

The static resource trap

Recording a real response and storing it as a static resource for StaticResourceCalloutMock is a good idea that ages badly. The file is a snapshot of one day in one environment, and nothing tells you when it stopped resembling reality. If you use them, re-record on a schedule and treat a changed file as something to read, not to commit blind.

Frequently asked

Can I make a real callout from a test at all? No. The platform blocks it. Anonymous Apex in a sandbox is how you do a manual round trip.

How do I test a retry that spans transactions? Test the decision logic in isolation, and test the queueable’s single pass with a mock. The chaining itself is best verified in a sandbox.

Is 75% coverage enough for an integration class? Coverage is the deployment gate, not the goal. An integration class with 100% coverage and no error-path assertions is untested in every way that matters.