Implementing HttpCalloutMock to respond to multiple HTTP requests

This post explains how to implement the Apex HttpCalloutMock interface to respond dynamically to any number and variety of HTTP request types in the same unit test.

If you’re wondering about the following …

  • How can I set up the HttpCalloutMock implementing class to return responses with different HTTP codes or responses?
  • How can I respond to more than one HTTP request in the same test (even with different response types)?
  • How can the whole setup be made easier, more configurable, and more reusable by my team?
    … then this post is for you.

Table of contents

  • Design overview for the HttpMultiMock service.
  • Build a factory for creating HTTP responses.
  • Store the HTTP responses in a data structure.
  • Match incoming HTTP requests with one of the stored responses.
  • Example: Testing api authentication and a subsequent request.
  • Further improvements.

Design overview for HttpMultiMock

In Salesforce, HTTP callouts have to be mocked in unit tests. This is achieved by implementing the HttpCalloutMock interface which has one method, respond(HttpRequest), which returns an HttpResponse object.

In the examples in this blog post, the implementing class is called HttpMultiMock.

The overall design comprises 3 parts:

  1. Http Response Factory - Firstly, the creation of HttpResponse objects is extracted into its own utility class - HttpResponseFactory.
  2. Http Response Storage - Secondly, a data structure is added to the HttpMultiMock class so that one or more HttpResponse objects can be loaded into it prior to a test.
  3. Http Response Matching - Lastly, a flexible system is implemented that allows the easy configuration (and reconfiguration) of multiple strategies to match an incoming HttpRequest object with one of the stored HttpResponses.

Build a factory for creating HTTP responses

Problem: Hard-coding restricts the range of possible responses

Creating an HttpResponse object directly in the respond() method isn't flexible. It's difficult to vary the shape of responses in a multi-request scenario. And as the response has been hard-coded, you can't reconfigure it for different tests.

public class MockResponseService implements HttpCalloutMock {

  public HttpResponse respond(HttpRequest req) {
    HttpResponse response = new HttpResponse();
    response.setStatusCode(200);
    // ... Not much configuration possible here
    return response;
  }
}

A first attempt at a more flexible solution

Decoupling response creation from the respond() method gives you the freedom to create as many different response types as you want. Whether the creation methods are in a separate class or not is a personal choice. I prefer to keep them separate as this reduces the amount of code and complexity in any one class. It also makes it plain for other developers where to add new response types.

As a first effort you could create some sort of super-flexible method with a large number of parameters:

public static HttpResponse CreateHttpResponse(Integer code, String status, String body, Map<String, String> responseHeaders) {
  HttpResponse response = new HttpResponse();
  response.setStatusCode(code);
  ... 
  return response;
}

But using this generic method directly is time consuming and error-prone. You have to get the fine details right every time in order to correctly set up the same given test scenario.

It may also not be obvious, when another developer comes to read the code, what the business context is. That will have to be worked out from the parameters. Again this is time consuming and liable to mistakes in interpretation, particularly for developers who are new to the business and the codebase.

// OK. So... I'm new here. Why is this set up this way?
// What is the business context behind this response?
HttpResponse response = CreateHttpResponse(200, 'OK', '{' +
    '"name": "Acme Builders",' +
    '"phone": "0123456789",' +
    '"shippingCountry": "UK"' +
'}');

Instead you can create methods that make the intention clear and simplify test setup. Even better, when requirements inevitably change, there will just be a single method to update.

public class HttpResponseFactory {

    // Method name provides business context behind
    // this scenario. Easy to get the test setup
    // right, and can be reused.
    public static HttpResponse GetLatestShippingDetailsSuccessResponse(String name, String phone, String shippingCountry) {
        String body = '{' +
            '"name": "' + name + '",' +
            '"phone": "' + phone + '",' +
            '"shippingCountry": "' + shippingCountry + '"' +
        '}';
        // Use generic method under the hood.
        return CreateHttpResponse(200, 'OK', body);
    }

    public static HttpResponse AuthenticationSuccessResponse() {
        String body = '{"accessToken": "test-access-token"}';
        return CreateHttpResponse(200, 'OK', body);
    }

    public static HttpResponse AuthenticationFailureResponse() {
        return CreateHttpResponse(401, 'Unauthorized', null);
    }
  
    // Many more methods where the meaning and
    // business context are clear...
}

Store the HTTP responses in a data structure

Now that the creation of response objects has been simplified, we need somewhere to store as many of them as required in the HttpMultiMock service so they're accessible during a test.

Using a List to store responses has limitations

One choice for this could be a List<HttpResponse>. As each request comes in, the respond() method could just pop the responses off in order:

public class HttpMultiMock implements HttpCalloutMock {
    List<HttpResponse> responseStore = new List<HttpResponse>();

    public respond(HttpRequest req) {
        return responseStore.remove(0);
    }
}

This may be sufficient for your needs, but it does make two assumptions. Firstly, that you can guarantee the order and number of the requests. Secondly, that each request is appropriate and well-formed, because the same response will always be returned at each stage no matter the validity of the request.

If you have set up the first response to be a successful authentication response, then it will always return a successful authentication response, even if the incoming request was, in fact, poorly formed and would have been rejected in production.

Using a Map to store responses gives us more flexibility

A more flexible solution that also entails some validation of the request, is to store the responses in a map.

Incoming HTTP requests will be translated, using one of the configured matching strategies (see next section), into a key which can then be used to get a response out of the responseStore. This translation process requires the requests to be correctly formed providing a form of validation.

public class HttpMultiMock implements HttpCalloutMock {

    Map<String, HttpResponse> responseStore = new Map<String, HttpResponse>();

    public respond(HttpRequest req) {
        // Use some strategy to translate the
        // information in the request to a key
        // that can be used to get the right
        // HttpResponse from the response store.
    }
}

Match incoming HTTP requests with one of the stored responses

As described, when the respond() method executes during the test, its logic needs to return the appropriate response based on the request. One option could be to write this logic directly in the respond() method, like so:

public class MockResponseService implements HttpCalloutMock {

    public respond(HttpRequest req) {
        if(req.getEndpoint().contains('authentication-server')) {
            // ... return AuthSuccess
        } else if(req.getEndpoint().endsWith('/accounts')) {
            // ... return response with list of accounts
        } else {
            System.assert(false, 'Invalid HttpRequest for test');
        }
      }
}

But, this approach is limited by lack of reconfiguration options and the fixed shape of the responses that are returned. Instead, now that the responseStore is storing whatever type and number of HttpResponse that we want, we can add a final step to make request-to-response-matching flexible and configurable.

Using the Strategy Pattern to create a configurable HttpMultiMock

In order to achieve this, you can extract each request-matching strategy into its own class. Each algorithm in this family implements a common interface: IHttpMatchingStrategy (this is an example of the Strategy Pattern).

In the test setup, one or more of these matchers can be configured for HttpMultiMock. As the HTTP requests come in, the matching algorithms will be executed in order until they find a corresponding response from the store.

Matching algorithms can be as generic, or as specific to your business context as you want. They translate an incoming HTTP request into a key that can be used to retrieve a response from the responseStore.

A MatchAuthRequest matcher might look for “authentication” in the endpoint URL. If found it will return the response stored in the responseStore using the ”auth” key.

A UrlParamMatcher may try to parse a record ID from the end of the URL path and search the responseStore using this record ID as the key.

An ExchangeRateApiMatcher may look into the JSON body of an incoming request for exchange rate api parameters and translate that data into a key to retrieve a response from the responseStore.

The possibilities are endless.

public interface IHttpMatchingStrategy {
    HttpResponse matchResponse(HttpRequest req, Map<String, HttpResponse> responseStore);
}
‌
/**
 * 2 examples of HTTP matching strategies that implement
 * the IHttpMatchingStrategy interface.
 */
@isTest
public class AuthMatcher implements IHttpMatchingStrategy {
  
    public HttpResponse matchResponse(HttpRequest req, Map<String, HttpResponse responseStore) {
        if(req.getEndpoint().contains('authentication-server')) {
            return responseStore.get('auth');
        } else {
            return null;
        }
    }
}

@isTest
public class UrlParamMatcher implements IHttpMatchingStrategy {
    
    public HttpResponse matchResponse(HttpRequest req, Map<String, HttpResponse> responseMap) {
        List<String> urlParts = req.getEndpoint().split('/');
        String recordID = urlParts[urlParts.size() - 1];
        return responseMap.get(recordID);
    }
}
‌
/**
 * Final version of the HttpMultiMock class that
 * can pre-load multiple responses and try multiple
 * response matching strategies to find the correct
 * HttpResponse.
 */
@isTest
public class HttpMultiMock implements HttpCalloutMock {

    private Map<String, HttpResponse> responseStore;
    private List<IHttpMatchingStrategy> responseMatchers;
  
    // I'm using a constructor here to set the responses
    // and matchers, but you could equally use setters,
    // or putResponse() and addMatcher() methods.
    public MockResponseService(Map<String, HttpResponse> responseStore, List<IHttpMatchingStrategy> matchers) {
        this.responseStore = responseStore;
        this.responseMatchers = matchers;
    }

    public HttpResponse respond(HttpRequest req) {

        HttpResponse response = matchResponse(req);
        if(response == null) {
            // Fail if no response found
            System.assert(false, 'No response found for request');
        }

        return response;
    }

    /**
     * Loop through the configured matching strategies
     * until a match is found.
     */
    private HttpResponse matchResponse(HttpRequest req) {

        HttpResponse response;
        for(IHttpMatchingStrategy matcher: responseMatchers) {
            response = matcher.matchResponse(req, responseStore);
            if(response != null) {
                break;
            }
        }

        return response;
    }
}

Example: Testing api authentication and a subsequent request

The following test sets up a very common scenario - Authenticating with an api, then making a request for data.

  1. First of all HttpResponseFactory utility methods are used to quickly generate appropriate HttpResponse objects for the test. The authentication success response is stored using the ”auth” key. The shipping details success response (which returns an Account record) is stored using an ID of ”testCompanyExtID” as the key.
  2. Two matchers are set up, AuthMatcher and UrlParameterMatcher.
  3. The HttpMultiMock instance is initialised with the responseStore and the matchers.
  4. The CompanyDataService’s getLatestShippingData() method is run during the test. My assumption is that the underlying implementation makes an authentication request, and then on-success makes a second request for shipping data.
  5. The authentication request is matched by the AuthMatcher, which then retrieves the correct response from the store using the ”auth” key.
  6. The AuthMatcher doesn’t match the next request for data from the shipping data api, but the UrlParamMatcher does, using the parsed URL param ”testCompanyExtID” to get the stored shipping details response.
@isTest
public class CompanyDataService_Test {
    
  @isTest
  private static void GetLatestShippingData_ApiCallIsSuccess() {

    // Setup HttpResponses in response store
    Map<String, HttpResponse> responseStore = new Map<String, HttpResponse>{
      'auth' => HttpResponseFactory.AuthenticationSuccessResponse(),
      'testCompanyExtID' => HttpResponseFactory.GetLatestShippingDetailsSuccessResponse(
        'testCompanyExtID',
        'Test Company Name',
        '0123456789',
        'United Kingdom'
      )
    };

    // Setup Matchers in the order they should be tried
    List<IHttpMatchingStrategy> matchers = new List<IHttpMatchingStrategy>{
      new AuthMatcher(),
      new UrlParamMatcher()
    };

    // Initialise HttpMultiMock
    HttpMultiMock mock = new HttpMultiMock(responseStore, matchers);

    // Test
    Test.startTest();
      Test.setMock(HttpCalloutMock.class, mock);
      CompanyDataService service = new CompanyDataService();
      // This authenticates and then gets the latest shipping data
      Account shippingData =
        service.getLatestShippingData('testCompanyExtID');
    Test.stopTest();

    // Assert
    System.assertEquals('Test Company Name', shippingData.Name);
    System.assertEquals('0123456789', shippingData.Phone);
    System.assertEquals('United Kingdom', shippingData.ShippingCountry);
  }
}

Further Improvements

There's certainly still scope to make it quicker and easier to set up consistent test scenarios, which will result in a lot more tests actually getting written and corner-cases tested.

One further improvement could be project-specific factories for setting up specific sets of responses and matchers, as shown below:

@isTest
public class CompanyDataService_Test {

  @isTest
  private static void GetLatestShippingData_ApiCallIsSuccess {
    
    // Setup
    HttpMultiMock mock = new HttpMultiMock(

      ResponsesFactory.AuthThenGetShippingData(
        'Test Company Name',
        '0123456789',
        'United Kingdom'
      ),
      MatchersFactory.ShippingApiMatchers()
    );
    
    // Test
    // ... test as per usual

    // Assert
	// .. assert as per usual
  }
}