Salesforce provides an interface for testing callouts named HttpCalloutMock used to cover remote callouts. While adequate for simple callouts, in the real world you often need something more flexible, as in the case of multiple and varying responses from the same or varying endpoints. More precise testing and coverage can be obtained by extending the standard interface. Join us as we demonstrate a solution to use to enable the flexibility required for complex integration and synchronization apps.
4. • Code we write…
• …about the code we just wrote
• Test classes
• Independent of code being tested
• One or more test methods
• @isTest annotation
Apex Test Coverage
“What” with a Hint of “How”
5. Apex Test Coverage
A Bit More of “How”
Grandma’s Famous Recipe
1. Write code to do
something
2. Create test class
3. Mark with @isTest
4. Create test method
5. Create test data
6. Invoke code being
tested
7. Assert (+ or -)
6. • Code we write…
• …about the code we just wrote
• Test classes
• Independent of code being tested
• One or more test method
Apex Test Coverage
Now More “What”
7. • You, me, everyone
• Required
• 75%
• Ongoing Development
• Help catch regressions
• Positive and negative assertions
Apex Test Coverage
“Who-ot” and “Why”
8. • As you’re developing
• Moving code from sandbox to production
• Creating packages
Apex Test Coverage
When
10. • Class w/ single method that makes a callout
Sample Scenario: Single Callout
Class Needing Coverage
11. Sample Scenario: Single Callout
Class Needing Coverage
Gist: https://gist.github.com/KirkSteffke/2ca4357c7e84eece9073
12. • Method making callout needs coverage
• Don’t want test methods to actually make callout!
• Responses to be used for positive/negative tests
Sample Scenario: Testing Objective
What We Need to Test
13. • Defined within test class
• Returns single callout
response
• WebServiceMock for WSDL
based SOAP callouts
• HttpCalloutMock for testing
Http callouts
• Defined by content of text
file in Static Resource
• Returns response for a
single endpoint
• Similar to
StaticResourceCalloutMock
• Static Resource contains
response for multiple
endpoints
WebServiceMock and
HttpCalloutMock Interfaces
StaticResourceCalloutMock
MultiStaticResourceCalloutMo
ck
Documentation: bit.ly/df15mock3Documentation: bit.ly/df15mock1 Documentation: bit.ly/df15mock2
Options for Testing HTTP Callouts
Out-of-the-box Tools
14. • Defined within test class
• Returns single callout
response
• WebServiceMock for WSDL
based SOAP callouts
• HttpCalloutMock for testing
Http callouts
• Defined by content of text
file in Static Resource
• Returns response for a
single endpoint
• Similar to
StaticResourceCalloutMock
• Static Resource contains
response for multiple
endpoints
WebServiceMock and
HttpCalloutMock Interfaces
StaticResourceCalloutMock
MultiStaticResourceCalloutMo
ck
Documentation: bit.ly/df15mock3Documentation: bit.ly/df15mock1 Documentation: bit.ly/df15mock2
Options for Testing HTTP Callouts
Out-of-the-box Tools
15. • Not “the” test, but is @test
• Called by test class methods
• Returns “mock” responses
• Sets up header, body, and status code
HttpCalloutMock Interface
Part 1: Mock Responder Class
17. • Test class for class making callout (part 1)
• Utilizes Mock Responder Class (part 2)
• Respond from callout comes from responder class (part 2)
HttpCalloutMock Interface
Part 2: Test Coverage
20. • Defined within test class
• Returns single callout
response
• WebServiceMock for WSDL
based SOAP callouts
• HttpCalloutMock for testing
Http callouts
• Defined by content of text
file in Static Resource
• Returns response for a
single endpoint
• Similar to
StaticResourceCalloutMock
• Static Resource contains
response for multiple
endpoints
WebServiceMock and
HttpCalloutMock Interfaces
StaticResourceCalloutMock
MultiStaticResourceCalloutMo
ck
Documentation: bit.ly/df15mock3Documentation: bit.ly/df15mock1 Documentation: bit.ly/df15mock2
Options for Testing HTTP Callouts
Out-of-the-box Tools
21. StaticResourceCalloutMock
Part 1: The static resource
• Only contains JSON text
• Can’t be a zipped static resource w/ multiple files
• mock.setStaticResource(‘Name of Zip’,’File in Zip’)
22. StaticResourceCalloutMock
Part 2: Test coverage
• Slightly different from last example
• No separate class; defined within coverage
• StaticResourceCalloutMock object instead of class with HttpCalloutMock interface
24. • Defined within test class
• Returns single callout
response
• WebServiceMock for WSDL
based SOAP callouts
• HttpCalloutMock for testing
Http callouts
• Defined by content of text
file in Static Resource
• Returns response for a
single endpoint
• Similar to
StaticResourceCalloutMock
• Static Resource contains
response for multiple
endpoints
WebServiceMock and
HttpCalloutMock Interfaces
StaticResourceCalloutMock
MultiStaticResourceCalloutMo
ck
Documentation: bit.ly/df15mock3Documentation: bit.ly/df15mock1 Documentation: bit.ly/df15mock2
Options for Testing HTTP Callouts
Out-of-the-box Tools
25. • Two methods making callouts
• One method using both
MultiStaticResourceCalloutMock
Part 1: Modified Scenario
26. • Similar to StaticResourceCalloutMock
• Define multiple endpoints
• Each has own Static Resource
• Still no Zip file
MultiStaticResourceCalloutMock
Part 2: Test Coverage
Static Resource: Example3_bar1 Static Resource: Example3_bar2
27. MultiStaticResourceCalloutMock
Part 2: Test Coverage
1. New MultiStaticResourceCalloutMock()
2. Set endpoint based resources
3. Set mock
4. Invoke callouts
5. Perform assertions
29. • Batch job
• Import of Contacts from remote system
• Remote system has an API
• Two endpoints
• /api/ContactCount
• /api/Contacts
Remote Import via Batch
Overview
30. • /api/ContactCount
• Returns # of Contact records to be imported
• Result can change during span of batch execution
• /api/Contacts
• Returns x number of contacts
• Paginated
• Page=1 – returns 1st 200 records
• Page=2 – returns 2nd 200 records
• …and so on
Remote Import via Batch
Meet the Endpoints
31. 1. Batch is called
2. Start() method
3. Each execute() iteration performs 2 callouts
4. 1st checks total # of records available
5. 2nd imports records
6. End of execute determines if more
records?
7. Execute may run for many iterations
8. If no more records or limits, finish()
9. Restart cycle
10. End
Remote Import via Batch
Batch Flow
33. Limitations
Working with Our Tools
• HttpCalloutMock Interface and StaticResourceMock
• Can only handle one endpoint
• Single response
• Single setup prior to invoking actual callouts
• MultiStaticResourceCalloutMock
• Handle multiple endpoints
• Single response for each
• Single setup prior to invoking actual callouts
34. Problems
Continued…
• Challenge
• Use one or more standard tool
• Extend the usage
• Create queue of expected responses per endpoint
• Consider re-usability
35. • MakeCallouts Method
• Loops 10x
• Builds string of results
• ContactCount Method
• Makes Callout
• Contacts Method
• Makes callout
• Page Parameter
Building the Problem
Example4_CalloutClass.apxc
public class Example4_CalloutClass {
// Method to simulate all the callouts in our batch flow
public static string MakeCallouts() {
// Property to return
string results = '';
// Let's call each resource 10x and...
for (integer i = 0; i < 10; i++) {
// ...add to results string this Count's response's body
results += 'Count (' + i + '): ' + CalloutContactCount().getBody() + 'rn';
// ...add to results string this Contact response's body
results += 'Contacts (' + i + '): ' + CalloutContacts(i).getBody() + 'rn';
}
// Return concatenated string of results
return results;
36. • ResponseMap
• By method
• By endpoint
• List of responses
• Respond
• Verify request
• Prepare response
• Discard
• Resp Class
Building the Solution
TestCalloutResponseGenerator.apxc
@isTest
global class TestCalloutResponseGenerator implements HttpCalloutMock {
// Property and getter (semi init'd) to pair method --> endpoint --> list of responses
private static map<string, map<string, list<resp>>> ResponseMap;
public static map<string, map<string, list<resp>>> getResponseMap() {
if (ResponseMap == null) {
ResponseMap = new map<string, map<string, list<resp>>>();
// For each setMethod() method type, pre-pop. w/ empty map
for (string method :new list<string>{'GET','PUT','POST','DELETE','HEAD','TRACE'})
ResponseMap.put(method, new map<string, list<resp>>());
}
return ResponseMap;
}
// Required respond() method for HttpCalloutMock
public HttpResponse Respond(HttpRequest req) {
37. • Body (JSON)
• Status (success)
• StatusCode
• Discard
Building the Solution
TestCalloutResponseGenerator.Resp
// Class to hold details of response from within test methods
public class Resp {
public string body { get; set; }
public string status { get; set; }
public integer statusCode { get; set; }
public boolean discard { get; set; }
public Resp(string body, string status, integer statusCode, boolean discard) {
this.body = body;
this.status = status;
this.statusCode = statusCode;
this.discard = discard;
}
}
38. • ResponseMap
• By method
• By endpoint
• List of responses
Building the Solution
TestCalloutResponseGenerator.getResponseMap()
// Property to pair method --> endpoint --> list of responses
private static map<string, map<string, list<resp>>> ResponseMap;
// Getter to return or prepare a semi init'd response map
public static map<string, map<string, list<resp>>> getResponseMap() {
if (ResponseMap == null) {
ResponseMap = new map<string, map<string, list<resp>>>();
// For each setMethod() method type, pre-pop. w/ empty map
for (string method :new list<string>{'GET','PUT','POST','DELETE','HEAD','TRACE'})
ResponseMap.put(method, new map<string, list<resp>>());
}
return ResponseMap;
}
39. • Respond
• Verify request
• Prepare response
• Discard
Building the Solution
TestCalloutResponseGenerator.Respond(HttpRequest req)
// Required respond() method for HttpCalloutMock
public HttpResponse Respond(HttpRequest req) {
// Property for returned response
HttpResponse res = new HttpResponse();
// Ensure HttpRequest is valid
if (req != null && !string.isBlank(req.getMethod()) && !string.isBlank(req.getEndPoint())) {
// Verify the Response map contains the req's method and endpoint
if (getResponseMap().containsKey(req.getMethod()) &&
getResponseMap().get(req.getMethod()).containsKey(req.getEndpoint())
) {
// Instantiate a list of the method/endpoint's response bodies
list<resp> respList =
getResponseMap().get(req.getMethod()).get(req.getEndpoint());
// If there's at least one, use it - otherwise, output an error
if (!respList.isEmpty()) {
40. • Setup ResponseMap
• Shortcut Explanation
• Contacts
• ContactCount
• Discard
• Set mock
• Invoke Callout
Testing the Solution
Example4_TestCalloutClass.TestWithPattern()
@isTest
public class Example4_TestCalloutClass {
public static testMethod void TestWithPattern() {
/* The below is just a shortcut for the demo. Instead of this 10x (1 per page):
TestCalloutResponseGenerator.getResponseMap().get('GET').put(
'/api/Contacts?page=1',
new list<TestCalloutResponseGenerator.Resp> {
new TestCalloutResponseGenerator.Resp(
'{"Contacts":"data...page 1'"}',
'success',
200,
false
)
}
);
*/
// Temporary collection to hold looped results (for demo, we don't care about actual data,
41. Testing the Solution
Example4_TestCalloutClass.TestWithPattern()
// Temporary collection to hold looped results (for demo, we don't care about actual data, just proof of concept)
map<string, list<TestCalloutResponseGenerator.Resp>> pagedResponses =
new map<string, list<TestCalloutResponseGenerator.Resp>>();
// Create a dummy response for Contact callout for our "10" pages of contacts
for (integer i = 0; i < 10; i++) {
// Each loop = 1 page
pagedResponses.put('/api/Contacts?page=' + i, new list<TestCalloutResponseGenerator.Resp>{
new TestCalloutResponseGenerator.Resp(
'{"Contacts":"data...' + i + '"}',
'success',
200,
true)
});
}
// Add all of the contact responses to the response map
TestCalloutResponseGenerator.getResponseMap().put('GET', pagedResponses);
43. Testing the Solution
TestCalloutClass
1. Count or Contact callout
2. # of loop iteration
3. Count data 10, 12, 12, …
1. Use 1st, discard
2. Use 2nd, don’t discard
4. Contact data increments
1. Provided per page response