Skip to main content

๐Ÿš€ Mastering Salesforce Mocking: HttpCalloutMock & StubProvider Explained

๐Ÿ’ฌ In plain words: Apex tests cannot make real callouts to external systems. Instead of hitting a live endpoint, you hand your test a "fake phone." Using HttpCalloutMock, you return a scripted response that you control. You use the exact same strategy for internal code dependencies using StubProvider. You are testing your own logic, not the uptime of an external system.

๐Ÿ”‘ Key Points

  • Apex completely blocks real outbound HTTP callouts during unit test execution.
  • HttpCalloutMock is used to intercept callouts at the transport layer and return a fake HttpResponse.
  • StubProvider is used to mock Apex dependencies at the object layer, allowing you to isolate and test specific business logic.
  • Mocking allows you to safely and predictably test success paths, error states (like 404 or 500 errors), and timeouts.
  • You must set your mock using Test.setMock() before the callout occurs in your test method.
๐ŸŽฌ Real-Life Example: The Test That Phoned a Real Bank

Imagine writing a test for a payment processing flow that attempts to ping a real external payment gateway.

The Old/Bad Way: Without a mock, the test either crashes (since callouts are blocked in tests) or the developer deliberately avoids testing the callout path. The result? The first time a declined credit card triggers a failure path is in production, affecting real users.

The New/Good Way:
  1. Create an HttpCalloutMock class that returns a carefully scripted HttpResponse.
  2. Use Test.setMock(HttpCalloutMock.class, new PaymentMock(200)) for your success path.
  3. Add a second test utilizing a mock that returns a 402 Payment Required status to purposefully validate your failure handling.
The Payoff: Your "fake phone" answers with whatever scenario you script. You can test your app's reaction to declined cards safely before it ever reaches production.
๐Ÿง  The Fake Phone Rule: Tests cannot call the outside world. Hand them a fake phone with HttpCalloutMock so they hear exactly the script you wrote.

Concept: Layers of Mocking

Because tests cannot make real network requests, Salesforce provides HttpCalloutMock (and WebServiceMock for SOAP) to intercept HTTP traffic at the transport layer. You tell the test context to use your mock by calling Test.setMock(), which then intercepts the request and hands back your predetermined response.

  • Transport Layer: Use HttpCalloutMock to validate how your code constructs a request (endpoints, headers, body) and parses a response.
  • Object Layer: Use StubProvider to mock Apex dependencies. This validates what your business logic actually does with the parsed results, without triggering the underlying database or network layers.
  • The Trap: Conflating the two layers leads to brittle tests that are overly focused on plumbing rather than behavior.
Mocking Callouts and Dependencies in Salesforce Apex
๐Ÿงญ 360 Card — Mocking Callouts & Dependencies
  • Rule: HttpCalloutMock fakes the wire. StubProvider fakes an Apex class. Pick your tool based on what you need to replace.
  • Gain: You can write tests for failure shapes that are impossible to trigger reliably on demand (e.g., server timeouts, 500 Internal Server errors, or retries that only succeed on the second attempt).
  • Reach For: HttpCalloutMock when your code makes a callout. StubProvider when you need to fake a class behavior. If interacting with a managed package, wrap the package class behind your own interface first, as you cannot stub what you cannot see.
  • Price: A mock permanently encodes your assumptions about the external system. If the third-party API contract changes, your green tests will lie to you because they are still passing against outdated assumptions.
  • Limits: Tests cannot make real callouts. Interception only happens at the transport layer.
  • At Volume: Make your mocks programmable (passing a scripted list of responses via the constructor) so you don't have to write a separate mock class for every single scenario.

Core Q&A

Q: Design the test strategy for a service that calls an external pricing API and applies the returned discount.
๐ŸŽฏ Say this first: Unit-test the discount logic with an injected fake response. Test the callout path with HttpCalloutMock for success, error, and timeout shapes.

A: Break it down into three tiers:

  • Tier One: HttpCalloutMock tests for the API client. Assert on the request it builds (endpoint, headers, body) and ensure it correctly parses successes, errors, timeouts, and malformed JSON into typed results. (Use MultiStaticResourceCalloutMock for multi-call sequences).
  • Tier Two: Logic tests that inject a stubbed client returning typed results. Assert the discount application across boundary cases with zero HTTP plumbing involved.
  • Tier Three: One integration-shaped test that wires the real service to the transport mock from end to end. Diagnostic precision is the goal here—each tier should only fail for one specific reason.
// 1. A fake HTTP service. Tests can never make a real callout.
@isTest
class PricingMock implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest req) {
        
        // 2. Check the code called the RIGHT endpoint.
        System.assert(req.getEndpoint().contains('/v2/price'));
        
        HttpResponse res = new HttpResponse();
        
        // 3. Script the reply. Change this to 500 to test the failure path.
        res.setStatusCode(200);
        res.setBody('{"discount": 12.5}');
        return res;
    }
}

// Inside your test method:
Test.setMock(HttpCalloutMock.class, new PricingMock());
⚠️ The Happy Path Trap: Writing an HttpCalloutMock that only returns a 200 OK status gives you a false sense of security. Always write negative tests that return 401s, 500s, or throw a CalloutException to ensure your Apex code doesn't crash when the external system goes down.
Q: How do you test failure modes like timeouts, 500s, and retry logic?

A: Make the mock programmable. Pass a scripted list of responses and behaviors into its constructor.

  • Throw a CalloutException to simulate a timeout.
  • Return a 500 status code, and then a 200 on the next invocation, to properly exercise a retry-to-success path.
  • Have the mock record its own invocation count. This allows your test to assert, "The system retried exactly 3 times before moving the record to a dead-letter queue."
  • If your retry logic lives in a Queueable chain, use Test.startTest() and Test.stopTest() to force asynchronous execution. Then assert against the logs or records that the Finalizer wrote.
Q: A managed package your code depends on cannot be stubbed. What are your options?

A: Wrap it. Define your own Apex interface that mirrors just the slice of the managed package API that you actually use.

  • Write a production implementation of this interface that delegates the actual work to the package. Then, inject the interface.
  • Your unit tests can now easily stub the wrapper.
  • The thin adapter class remains as the only glue code lacking unit test coverage—cover it with an integration test instead.
  • This identical pattern handles other unstubbable elements like static methods and standard System classes.
  • Bonus Insight: The wrapper pattern not only enables testability but also insulates your core codebase from unexpected changes in the managed package's API.
Q: How do you handle testing multiple different callouts within the same transaction?

A: When your code hits multiple different endpoints in a single run, a standard mock will struggle because it needs to know which response to give. You have two main options:

  • Custom Routing Mock: Write a custom HttpCalloutMock that inspects req.getEndpoint() and uses if/else or a switch statement to return the correct response for each specific URL.
  • MultiStaticResourceCalloutMock: Use this built-in Salesforce class. It allows you to map specific endpoints directly to different Static Resources containing your mock JSON responses, completely eliminating the need for complex custom routing logic.
Q (Compare): HttpCalloutMock vs StubProvider — which one do you use when?

A: HttpCalloutMock fakes the WIRE. It returns the HTTP response your callout code will see. StubProvider fakes a CLASS. It stands in for any dependency hiding behind an interface.

  • Testing callout transport code? Mock the wire.
  • Testing complex business logic that uses an injected dependency? Stub the class.