Testing complex integrations in Salesforce often presents a unique challenge: what happens when a single transaction requires multiple callouts to different services? In a typical real-world scenario, you might need to call Service A to retrieve an OAuth token, then use that token to query Service B, and perhaps even log the result in Service C.
While Salesforce provides the Test.setMock method to simulate these external calls, developers quickly run into a limitation: you can only register one mock implementation per interface type (either WebServiceMock or HttpCalloutMock) within a single test execution. If you call Test.setMock a second time, it simply overwrites the first one.
In this guide, you will learn how to architect sophisticated mock handlers that can dispatch responses for multiple endpoints, ensuring your unit tests remain robust, maintainable, and highly performant.
The Challenge: The Single Mock Limitation
When writing Apex unit tests, the Test.setMock method is your primary tool for bypassing actual network calls. However, because a test method only recognizes the last mock registered, a sequential workflow involving different endpoints will fail if you try to set individual mocks for each step.
To solve this, you must implement a "Dispatcher" or "Router" pattern. Instead of providing a specific mock for one endpoint, you provide a single, intelligent mock class that inspects the incoming request and routes it to the appropriate response logic.
Strategy 1: The Dispatcher Pattern for SOAP Services
If you are working with SOAP-based web services (generated from a WSDL), you use the WebServiceMock interface. The core of this interface is the doInvoke method. This method receives several parameters, including the stub (the service class) and the request (the actual data being sent).
By checking the type of the stub or the request object using the instanceof keyword, you can delegate the response logic to sub-classes. This keeps your code organized and prevents a single mock class from becoming a massive, unreadable file.
Test.setMock(WebServiceMock.class, new MockDispatcher());
public class MockDispatcher implements WebServiceMock
{
public void doInvoke(
Object stub, Object request, Map<String, Object> response,
String endpoint, String soapAction, String requestName,
String responseNS, String responseName, String responseType)
{
if(stub instanceof ServiceA)
new ServiceAMock().doInvoke(
stub, request, response,
endpoint, soapAction, requestName,
responseNS, responseName, responseType);
else if(stub instanceof ServiceB)
new ServiceBMock().doInvoke(
stub, request, response,
endpoint, soapAction, requestName,
responseNS, responseName, responseType);
return;
}
}
public class ServiceAMock
{
public void doInvoke(
Object stub, Object request, Map<String, Object> response,
String endpoint, String soapAction, String requestName,
String responseNS, String responseName, String responseType)
{
if(request instanceof ServiceA.GetAuthTokenRequest_element)
response.put('response_x', new ServiceA.GetAuthTokenResponse_element());
return;
}
}
In this example, MockDispatcher acts as a traffic controller. When Service A is called, it identifies the stub and passes the parameters to ServiceAMock. This allows you to test the entire multi-service chain in one go.
Strategy 2: Dynamic HTTP Callout Mocking for REST
For RESTful integrations using HttpCalloutMock, the approach is slightly different. Since REST calls are identified primarily by their URL and HTTP method (GET, POST, etc.), you can build a generic mock generator that uses a Map to correlate endpoints with specific responses.
This approach is highly reusable across different test classes and is much easier to maintain than hard-coding logic inside the respond method.
@isTest
global class MockHttpResponseGenerator implements HttpCalloutMock {
private Map<String, String> responseMap = new Map<String, String>();
public Integer statusCode {get; set;} {statusCode = 200;}
public MockHttpResponseGenerator() {}
public void addResponse(String endpoint, String responseBody) {
responseMap.put(endpoint, responseBody);
}
global HTTPResponse respond(HTTPRequest req) {
String endpoint = req.getEndpoint();
String responseBody = responseMap.get(endpoint);
if (responseBody == null) {
System.assert(false, 'No mock response defined for endpoint: ' + endpoint);
}
HttpResponse res = new HttpResponse();
res.setHeader('Content-Type', 'application/json');
res.setBody(responseBody);
res.setStatusCode(statusCode);
return res;
}
}
To use this in your test, you simply register the endpoints you expect to hit:
MockHttpResponseGenerator mock = new MockHttpResponseGenerator();
mock.addResponse('https://auth.service.com/token', '{"token": "12345"}');
mock.addResponse('https://api.service.com/data', '{"status": "success"}');
Test.setMock(HttpCalloutMock.class, mock);
Strategy 3: Decoupling with Type-Based Injection
One common issue with complex mock setups is the risk of circular references, where your production code references a test class to determine which mock to use. To avoid this, you can use the Type class to dynamically instantiate mocks without creating a hard dependency.
By defining an abstract base class for your API calls, you can check Test.isRunningTest() and inject the appropriate mock type dynamically. This is particularly useful for enterprise-level applications with dozens of integration points.
public abstract class BaseIntegration {
public Object sendRequest(String mockTypeName) {
if (Test.isRunningTest()) {
Type t = Type.forName(mockTypeName);
if (t != null) {
Test.setMock(HttpCalloutMock.class, (HttpCalloutMock)t.newInstance());
}
}
// Proceed with actual callout logic...
}
}
Note that while Test.isRunningTest() is often discouraged in production logic, it is a pragmatic solution when dealing with the strict limitations of the Test.setMock architecture in multi-callout scenarios.
Frequently Asked Questions
Can I call Test.setMock twice for the same interface?
You can, but it is not effective. The Salesforce runtime only retains the most recent mock registered. If your code executes two callouts, both will attempt to use the second mock you registered, which often leads to TypeCast exceptions or unexpected null values.
How do I handle different HTTP status codes for different endpoints?
To handle varying status codes (e.g., a 200 for auth but a 404 for a missing record), you should modify your MockHttpResponseGenerator to store an object containing both the body and the status code in your map, rather than just a string.
Does this work for both JSON and XML?
Yes. The dispatcher and generator patterns are format-agnostic. For SOAP (XML), you use the WebServiceMock interface and manipulate the response map. For REST (JSON/XML), you use the HttpCalloutMock interface and set the response body string directly.
Wrapping Up
Testing multi-step integrations doesn't have to be a headache. By implementing a Dispatcher pattern for SOAP or a dynamic Map-based generator for REST, you can simulate complex workflows with ease.
Key takeaways to remember:
- Consolidate your mocks: Use one main mock class to route requests to specialized handlers.
- Inspect the request: Use instanceof for SOAP stubs or req.getEndpoint() for REST requests to determine the correct response.
- Keep it clean: Use sub-classes or helper methods to define specific response payloads, keeping your dispatcher class readable.
By following these patterns, you ensure that your Salesforce integrations are fully covered by unit tests, leading to more stable deployments and a more reliable codebase.