Unit tests that need more than one WebServiceMock
The doInvoke method on the mock interface passes in the stub and request parameters. You can use these to condition to dispatch the mock logic to various other mock implementations, while only registering a single mock implementation.
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 A)
new AMock().doInvoke(
stub, request, response,
endpoint, soapAction, requestName,
responseNS, responseName, responseType);
else if(stub instanceof B)
new BMock().doInvoke(
stub, request, response,
endpoint, soapAction, requestName,
responseNS, responseName, responseType);
return;
}
}
public class AMock
{
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 A.GetAuthTokenRequest_element)
response.put('response_x', new A.GetAuthTokenResponse_element());
return;
}
}
public class BMock
{
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 B.SomeMethodRequest_element)
response.put('response_x', new B.SomeMethodResponse_element());
return;
}
}
I've found I've had to add something like this in my actual Class methods to switch my testMock if I'm running a test.
public String getActivityFile() {
if(test.isRunningTest) {
Test.setMock(WebServiceMock.clas, new WebserviceNumber2Mock());
}
return new WebserviceNumber2().getActivityFile();
}
Update 2018-05-22
Extending this idea, it's a pain to deal with circular references when referencing a Test class in a Production class. However, you can references a Test Class Type
without causing a direct reference to another class. As of today (API 42.0), Types are not compiled when saving Apex. So you can do something like the following:
public abstract class SoapApi {
public interface Request {
Type getType();
Type getResponseType();
Type getMockResponseType();
String getSoapAction();
String getRequestNamespace();
String getResponseNamespace();
String getResponseName();
}
public abstract class MockResponse implements WebServiceMock {
// Abstract
public abstract Object getResponse(Request request);
protected Object response;
// WebServiceMock
public void doInvoke(
Object stub,
Object request,
Map<String, Object> response,
String endpoint,
String soapAction,
String requestName,
String responseNamespace,
String responseName,
String responseType
) {
if(!(request instanceof Request)) {
new InvalidMockRequestException(System.Label.SoapApi_InvalidMockRequestException); // 'Requestable must implement SoapApi.Requestable'
}
this.response = this.getResponse((Request) request);
response.put('response_x', this.response);
}
}
// Abstract
public abstract String getEndpoint();
// Instance
public Object send(SoapApi.Request request) {
if(request == null) {
throw new NullRequestException(System.Label.SoapApi_NullRequestException); // 'Request cannot be null'
}
if(request.getType() == null) {
throw new NullRequestTypeException(System.Label.SoapApi_NullRequestTypeException); // 'Request\'s Type cannot be null'
}
if(Test.isRunningTest()) {
Object response = request.getMockResponseType() == null ? null : request.getMockResponseType().newInstance();
if(!(response instanceof MockResponse)) {
throw new InvalidMockResponseException(System.Label.SoapApi_InvalidMockResponseException); // 'Request\'s Mock Response Type must extend SoapApi.MockResponse'
}
Test.setMock(WebServiceMock.class, response);
}
// Invoke WebServiceCallout
Map<String, Object> responses = new Map<String, Object> {
'response_x' => null
};
try {
WebServiceCallout.invoke(
this,
request,
responses,
new String[] {
this.getEndpoint(),
request.getSoapAction(),
request.getRequestNamespace(),
request.getRequestName(),
request.getResponseNamespace(),
request.getResponseName(),
request.getResponseType().getName()
}
);
} catch(Exception e) {
// Can do debugging here else remove the Try/Catach
throw e;
}
return responses.get('response_x');
}
}
public with sharing class ThirdPartyData {
public with sharing class LocationRequest implements SoapApi.Request {
public String address;
@TestVisible String[] address_type_info = new String[] {/* ... */};
@TestVisible String[] apex_schema_type_info = new String[] {/*...*/};
@TestVisible String[] field_order_type_info = new String[] {
'address'
};
// SoapApi.Request
public Type getType() {
return ThirdPartyData.LocationRequest.class;
}
public Type getResponseType() {
return return ThirdPartyData.LocationResponse.class;
}
public Type getMockResponseType() {
return return ThirdPartyDataMock.LocationResponse.class;
}
public String getSoapAction() {
return String.join(
new String[] {
'https://api.example.com',
'ThirdPartData',
'getLocation'
},
'/'
);
}
public String getRequestNamespace() {
return 'ThirdPartyData';
}
public String getResponseNamespace() {
return 'ThirdPartyData';
}
public String getResponseName() {
return 'LocationResponse';
}
}
public with sharing class LocationResponse {
public String address, city, state, postal;
@TestVisible String[] address_type_info = new String[] {/* ... */};
@TestVisible String[] city_type_info = new String[] {/* ... */};
@TestVisible String[] state_type_info = new String[] {/* ... */};
@TestVisible String[] postal_type_info = new String[] {/* ... */};
@TestVisible String[] apex_schema_type_info = new String[] { /*...*/};
@TestVisible String[] field_order_type_info = new String[] {
'address',
'city',
'state',
'postal'
};
}
// Instance
public with sharing class Api extends SoapApi {
public Integer timeout_x = 60000; // Milliseconds
public String[] ns_map_type_info = new String[] {
'https://api.example.com', 'ThirdPartData',
};
public Map<String, String> inputHttpHeaders_x;
public Api() {
super();
}
// SoapApi
public override String getEndpoint() {
return 'callout:packagenamepace__ThirdPartData/Api.svc';
}
public LocationResponse getLocation(String address) {
LocationRequest request = new LocationRequest();
{
request.address = address;
}
return (LocationResponse) this.send(request);
}
}
}
@IsTest
public with sharing class ThirdPartyDataMock {
public with sharing class InvalidMockRequestException extends Exception {}
public with sharing class LocationResponse extends SoapApi.MockResponse {
public override Object getResponse(SoapiApi.Request request) {
if(!(request instanceof ThirdPartyData.LocationRequest)) {
throw new InvalidMockRequestException(String.format(
System.Label.ThirdPartyDataMock_InvalidMockRequestException, // '{0} only supports {1}'
new String[] {
ThirdPartyDataMock.LocationResponse.class.getName(),
ThirdPartyData.LocationRequest.class.getName()
}
));
}
ThirdPartyData.LocationRequest locationRequest = (ThirdPartyData.LocationRequest) request;
ThirdPartyData.LocationResponse response = new ThirdPartyData.LocationResponse();
{
response.address = address;
response.city = 'Anytown';
response.state = 'MI';
response.postal = '49000';
}
return response;
}
}
}
I saw this technique from Andrew a little while back, and it has been a lifesaver for testing complex WebServices. One addition to his answer is that I set the response directly in the mock class e.g.
if(request instanceof MyWebService.GetService_element) {
MyWebService.GetServiceResponse_element testresponse_x = new MyWebService.GetServiceResponse_element();
testresponse_x.GetServiceResult = MyMockTestResponses_TESTS.getTestValues();
response.put('response_x', testresponse_x);
}
Where GetService is the method in the WebService and MyMockTestResponses.getTestValues() is a returning a static JSON/XML/String that has been set previously in my test classes.