Apex Test Mocking framework with STUB API
Test mocking frameworks allows developer to perform unit testing by providing a way to create mock objects that mimic the response/ output of a function or service. This allows developer to isolate the code unit and test logic inside the same without needing to perform DML operation or relying on actual implementation of their dependencies. Mocking framework can streamline and improve testing and create faster and reliable tests.
Developer can implement their own mocking framework by using Stub API.
Leveraging the test mocking framework becomes easier with separation of concerns design principle. Meaning our code should have following layer implemented to fully leverage any mocking framework,
- Selector layer: This layer has the queries to retrieve the data from object. This allows us to reuse the queries as well as builds a central location to look for in case of troubleshooting and adjustment.
- Domain Layer:This layer deals with Data manipulation.
- Service Layer: The service layer deals with the business logic. This layer interacts with selector layer, domain layer and some times service layer code as well
- Implementation Layer: The implementation layer deals with number of ways you can call apex code across the Salesforce platform. Apex Controller is most common example of implementation layer. A Controller methods invokes the Service layers code.
Once you segregate your code in these layers then you are all set to implement and use the mocking framework.
If you are working on existing project/codebase where separation of concern is not followed then you can start using the mocking framework at smaller level. for ex. you might be working on the new feature for which business logic never been implemented. Then this can be a good use case to implement separation of concern on smaller level to use test mocing framework.
Following is the small example to implement the stub API and illustrate the mechanism of the same.
Let’s say we want to test the calculation of expected revenue. Following class has implemented a method calculateExpectedRevenue. This method calculate expected revenue by fetching the opportunity record and then calculates the expected revenue by following formulae,
Expected Revenue=Amount * Probability
public class ExpectedRevenue {
public double calculateExpectedRevenue(OpportunitySelectorLayer oppSelector)
{
Opportunity opp=oppSelector.getOpportunity();
double expectedRevenue=opp.Amount*opp.Probability;
return expectedRevenue;
}
}
calculateExpectedRevenue method uses opportunity selector class to fetch the opportunity. Hence, we pass OpportunitySelectorLayer instance to calculateExpectedRevenue Method.
public class OpportunitySelectorLayer {
public Opportunity getOpportunity()
{
return [select id,Amount,Probability from opportunity
where Amount !=null and Probability!=null limit 1];
}
}
The following code invokes the method.
OpportunitySelectorLayer oppSelector=new OpportunitySelectorLayer();
ExpectedRevenue expRev=new ExpectedRevenue();
id accId=[select id from Account limit 1].id;
expRev.calculateExpectedRevenue(oppSelector,accId);
In above example,
- calculateExpectedRevenue logic depends on the output of getOpportunity method.
- Output of Method getOpportunity can vary.
To perform unit testing of method calculateExpectedRevenue,We need to have constant response from getOpportunity method.
This can be achieved by,
- Writing fake version of the class OpportunitySelectorLayer which will return constant response for method getOpportunity. OR
- Using a stub API to dynamically create stub object and specifying stubbed behaviour of method getOpportunity
Using stub API is more relevant (Point 2 above). Hence, we will see how to use a stub version of an Apex class. Following three steps needs to be completed to use Stub API and dynamically create stub object.
- Define the behaviour of the stub class by implementing the System.StubProvider interface.
- Instantiate a stub object by using the System.Test.createStub() method.
- Invoke the relevant method of the stub object from within a test class.
Stub API Implementation:
Stub API can be used by implementing System.StubProvider interface and System.Test.createStub() method.
System.StubProvider is a Callback Interface which specifies a single method i.e handleMethodCall(). You can define behavior of the stubbed class in this method. This methods gets invoked when test class calls the stubbed method.
- stubbedObject: The stubbed object
- stubbedMethodName: The name of the invoked method
- returnType: The return type of the invoked method
- listOfParamTypes: A list of the parameter types of the invoked method
- listOfParamNames: A list of the parameter names of the invoked method
- listOfArgs: The actual argument values passed into this method at runtime
You can identify and define different behavior of methods depending on the return type i.e returnType , parameters i.e listOfParamTypes, listOfParamNames,listOfArgs or name of method i.e stubbedMethodName.
Stub provider implementation,
@isTest
public class StubMockProvider implements System.StubProvider {
public Object handleMethodCall(Object stubbedObject, String stubbedMethodName,
Type returnType, List<Type> listOfParamTypes, List<String> listOfParamNames,
List<Object> listOfArgs) {
// The following debug statements show an example of logging
// the invocation of a mocked method.
// You can use the method name and return type to determine which method was called.
System.debug('Name of stubbed method: ' + stubbedMethodName);
System.debug('Return type of stubbed method: ' + returnType.getName());
// You can also use the parameter names and types to determine which method
// was called.
for (integer i =0; i < listOfParamNames.size(); i++) {
System.debug('Parameter name: ' + listOfParamNames.get(i));
System.debug('Parameter type: ' + listOfParamTypes.get(i).getName());
}
// This shows the actual parameter values passed into the stubbed method at runtime.
System.debug('number of parameters passed into the mocked call: ' +
listOfArgs.size());
System.debug('parameter(s) sent into the mocked call: ' + listOfArgs);
// returns a hard-coded opportunity object
// based on the return type of the invoked method.
if (returnType.getName() == 'Opportunity')
return new Opportunity(id='0067Q00000KOVBbQAP',Amount=5000,Probability=5);
else
return null;
}
}
In above implementation, We are using returnType.getName() to identify the return type of stubbed method. If return type of stubbed method in ‘Opportunity’ then we return hard coded opportunity object.
Instantiate a stub object
The next step is to instantiate a stub object by using the System.Test.createStub() method. The following utility class returns a stub object that we can use as a mock.
public class Mock_Util {
private Mock_Util(){}
public static StubMockProvider getInstance() {
return new StubMockProvider();
}
public static Object createMock(Type typeToMock) {
// Invoke the stub API and pass it our mock provider to create a
// mock class of typeToMock.
return Test.createStub(typeToMock, Mock_Util.getInstance());
}
}
This class contains the method createMock(), which invokes the Test.createStub() method. The createStub() method takes an Apex class type and an instance of the StubProvider interface that we created previously. It returns a stub object that we can use in testing.
Invoke Stub Method
At last, we invoke the relevant method of the stub class from within a test class.In this test, we call the createMock() method to create a stub version of the OpportunitySelectorLayer class. We can then invoke the getOpportunity() method on the stub object, which returns our hard-coded Opportunity Object. Using the hard-coded Opportunity allows us to test the behavior of the calculateExpectedRevenue() method in isolation.
@isTest
public class ExpectedRevenueTest {
@isTest
public static void testCalculateExpectedRevenue() {
// Create a mock version of the DateHelper class.
OpportunitySelectorLayer mockOppSelector = (OpportunitySelectorLayer)Mock_Util.createMock(OpportunitySelectorLayer.class);
ExpectedRevenue exRev = new ExpectedRevenue();
// Use the mocked object in the test.
System.assertEquals(25000, exRev.calculateExpectedRevenue(mockOppSelector));
}
}
Limitations
Apex stub API limitations are as follows,
- The object being mocked must be in the same namespace as the call to the Test.createStub() method. However, the implementation of the StubProvider interface can be in another namespace. for. example Mock_Util.cls should be in same namespace as the OpportunitySelectorLayer.cls.
- Iterators can’t be used as return types or parameter types.
- You can’t mock the following Apex elements.
- Static methods (including future methods)
- Private methods
- Properties (getters and setters)
- Triggers
- Inner classes
- System types
- Classes that implement the Batchable interface
- Classes that have only private constructors
- Iterators can’t be used as return types or parameter types.
Reference: