The Winter ’13 release added a couple of new features for testing Apex callouts. Specifically, developers can now either
- Use a Static Resource to save the test response (e.g. a JSON text file) and then use the StaticResourceCalloutMock or MultiStaticResourceCalloutMock system classes to test one or more callouts, or
- Implement one of two new interfaces: HttpCalloutMock, for HTTP (REST) callouts, or WebServiceMock, for Web Service (SOAP) callouts
Pat wrote an excellent blog post breaking down the first option. Lets review the second option, specifically how to implement the HttpCalloutMock interface to test HTTP callouts.
Testing a single HTTP callout
Lets start with a relatively simple requirement of testing an Apex class that makes a single HTTP (i.e. REST) callout. Here’s the code that we need to test.
public class CalloutAccounts {
public static List<Account> getAccounts() {
HttpRequest req = new HttpRequest();
req.setEndpoint('http://api.example.com/accounts');
req.setMethod('GET');
Http h = new Http();
HttpResponse res = h.send(req);
String jsonData = res.getBody();
List<Account> accounts =
(List<Account>)JSON.deserialize(jsonData, List<Account>.class);
return accounts;
}
}
The first step is to create a class that implement the HttpCalloutMock interface.
@isTest
public class SingleRequestMock implements HttpCalloutMock {
protected Integer code;
protected String status;
protected String bodyAsString;
protected Blob bodyAsBlob;
protected Map<String, String> responseHeaders;
public SingleRequestMock(Integer code, String status, String body,
Map<String, String> responseHeaders) {
this.code = code;
this.status = status;
this.bodyAsString = body;
this.bodyAsBlob = null;
this.responseHeaders = responseHeaders;
}
public SingleRequestMock(Integer code, String status, Blob body,
Map<String, String> responseHeaders) {
this.code = code;
this.status = status;
this.bodyAsBlob = body;
this.bodyAsString = null;
this.responseHeaders = responseHeaders;
}
public HTTPResponse respond(HTTPRequest req) {
HttpResponse resp = new HttpResponse();
resp.setStatusCode(code);
resp.setStatus(status);
if (bodyAsBlob != null) {
resp.setBodyAsBlob(bodyAsBlob);
} else {
resp.setBody(bodyAsString);
}
if (responseHeaders != null) {
for (String key : responseHeaders.keySet()) {
resp.setHeader(key, responseHeaders.get(key));
}
}
return resp;
}
}
Note: While it is not required to mark your HttpCalloutMock implementation class as @isTest, it is generally a best practice to do so in order to exclude the class from your organization’s code size limit of 3 MB.
You need to implement the ‘respond’ method of the HttpCalloutMock interface in which you return a fake HttpResponse. As you can see, we’ve created a utility testing class that can be used to test any scenario that involves a single HTTP callout, and not just the CalloutAccounts class shown earlier. This utility class can be used to test both binary and String responses (depending on which constructor you use to instantiate the class) and also lets you specify the HTTP code and status for the fake response. You can optionally also specify HTTP headers to be included in the fake response. Next, lets see how we use this HttpCalloutMock implementation to test our CalloutAccounts class.
@isTest
public class CalloutAccountsTest{
public static testmethod void testAccountCallout() {
SingleRequestMock fakeResponse = new SingleRequestMock(200,
'Complete',
'[{"Name": "sForceTest1"}]',
null);
Test.setMock(HttpCalloutMock.class, fakeResponse);
CalloutAccounts.getAccounts();
System.assertEquals(/*check for expected results here...*/);
}
}
Line 8 shows how we use the Test.setMock system method to tell the platform which HttpCalloutMock implementation to use during the test. After that line, if an HTTP callout is invoked in test context (e.g. the callout code in our CalloutAccounts.getAccounts method), the callout is not made and you receive the mock response specified in the ‘respond’ method implementation.
Testing multiple HTTP callouts
public class ProcessAccountsContacts {
public static String getJSON(String url) {
HttpRequest req = new HttpRequest();
req.setEndpoint(url);
req.setMethod('GET');
Http h = new Http();
HttpResponse res = h.send(req);
return res.getBody();
}
public static Integer processAccountsContacts() {
String jsonData = getJSON('http://api.example.com/accounts');
List accounts =
(List)JSON.deserialize(jsonData, List.class);
jsonData = getJSON('http://api.example.com/contacts');
List contacts =
(List)JSON.deserialize(jsonData, List.class);
// 'Processing'
Integer result = accounts.size() + contacts.size();
return result;
}
}
public class MultiRequestMock implements HttpCalloutMock {
Map<String, HttpCalloutMock> requests;
public MultiRequestMock(Map<String, HttpCalloutMock> requests) {
this.requests = requests;
}
public HTTPResponse respond(HTTPRequest req) {
HttpCalloutMock mock = requests.get(req.getEndpoint());
if (mock != null) {
return mock.respond(req);
} else {
throw new MyCustomException('HTTP callout not supported for test methods');
}
}
public void addRequestMock(String url, HttpCalloutMock mock) {
requests.put(url, mock);
}
}
@isTest
public class MultipleCalloutsTest{
public static testmethod void testAcctsAndContactsCallout() {
SingleRequestMock fakeAccountResp = new SingleRequestMock(200,
'Complete',
'[{"Name": "sForceTest1"}]',
null);
SingleRequestMock fakeContactsResp = new SingleRequestMock(200,
'Complete',
'[{"LastName": "Test Last Name"}]',
null);
Map<String, HttpCalloutMock> endpoint2TestResp =
new Map<String,HttpCalloutMock>();
endpoint2TestResp.put('http://api.example.com/accounts',fakeAccountResp);
endpoint2TestResp.put('http://api.example.com/contacts',fakeContactsResp);
HttpCalloutMock multiCalloutMock =
new MultiRequestMock(endpoint2TestResp);
Test.setMock(HttpCalloutMock.class, multiCalloutMock);
ProcessAccountsContacts.processAccountsContacts();
System.assertEquals(/*check for expected results here...*/);
}
}