Salesforce's Stub API
Salesforce's stub API is a powerful tool that allows developers to mock out the behavior of their Apex code during testing. This helps to eliminate external dependencies and makes it easier to test code in isolation. In this article, we will explore the basics of the stub API and how it can be used to improve the reliability and speed of your Salesforce development process.
What is the Stub API?
The Salesforce Stub API is a powerful tool that can greatly enhance the functionality of your Salesforce instance. It allows you to create and manage virtualized versions of Salesforce objects, such as accounts, contacts, and opportunities, which can be used for testing and development purposes. Not only that, but with the Stub API, you can easily create mock data for use in automated testing, or develop and test new features and customizations without affecting your production data.
Salesforce’s Stub API consists of two main elements: the System.StubProvider interface and the System.Test.createStub() method.
Since this is indeed a topic for advanced Apex developers, we are going to cover the interface StubProvider in more detail. StubProvider allows you to define the behavior of a stubbed Apex class. It specifies a single method that requires implementation. By using the method Test.createStub(), you are able to create stubbed Apex objects for testing.
Benefits
Limitations
Not everything can be great! The Stub API has a few limitations, listed below:
How does the Stub API work? Usage and example
Stub API follows a design pattern called dependency injection, which, as its name says, consists of injecting dependencies into your classes or methods instead of calling the concrete classes directly from within the classes or methods. Therefore, this technique enables developers to have decoupled code and have better unit testing.?
To use a stub version of an Apex class, follow these steps:
To showcase these 3 steps we just mentioned, we are going to use an example.
/**
?* @description A class to summarize data for a list of accounts.
?* @author [email protected]
?* @date 02-01-2023
?*/
public with sharing class AccountService {
?
?private AccountDomain domain;
?
?/**
??* @description Constructor to initialize the `domain` property.
??*/
?public AccountService() {
???this.domain = new AccountDomain();
?}
?
?/**
??* Set the `domain` property.
??* @param domain The `AccountDomain` to set.
??*/
?public void setAccountDomain(AccountDomain domain) {
???this.domain = domain;
?}
?
?/**
??* Summarize data for a list of accounts.
??* @param accounts The list of accounts to summarize data for.
??*/
?public void summarizeData(List<Account> accounts) {
?????List<Account> accountsToUpdate = new List<Account>();
?
?????Map<Id, Integer> totalWonOpportunities = this.domain.sumWonOpps(accounts);
?
?????for (Id accountId : totalWonOpportunities.keySet()) {
???????accountsToUpdate.add(
?????????new Account(
???????????Id = accountId,
???????????Won_Opportunities__c = totalWonOpportunities.get(accountId)
?????????)
???????);
?????}
?
?????update accountsToUpdate;
?}
}
The AccountService class has a private instance variable called domain of type AccountDomain, which is another class. It has a default constructor that initializes this variable to a new instance of AccountDomain.
The class also has a method called setAccountDomain that allows the caller to set the value of the domain variable to an instance of AccountDomain provided as an argument. This method is necessary in order to proceed with the stubbing.
/**
?* @description A class to contain domain logic for Account objects.
?* @author [email protected]
?* @date 02-01-2023
*/
public with sharing class AccountDomain {
?
?/**
??* @description Sum the number of won opportunities for a list of ? accounts.
??* @param accounts The list of accounts to summarize data for.
??* @return A map of account IDs to the number of won opportunities.
??*/
?public Map<Id, Integer> sumWonOpps(List<Account> accounts) {
???Map<Id, Integer> resultMap = new Map<Id, Integer>();
???Map<Id, Account> accountsMap = new Map<Id, Account>(accounts);
?
???for (AggregateResult agr : [select count(Id), AccountId from Opportunity where Id IN :accountsMap.keySet() AND IsWon = true group by AccountId]) {
?????Id key = Id.valueOf(String.valueOf(agr.get('AccountId')));
?????Integer value = Integer.valueOf(agr.get('expr0'));
?????resultMap.put(key, value);
???}
?
???return resultMap;
?}
}
The AccountDomain class has one public method called sumWonOpps, which takes a list of Account objects as an input and returns a map of account IDs to the number of won opportunities.?
The method uses an AggregateResult query to count the number of opportunities that are won for each account, and then stores the results in a map. This method is the one that is going to be used for the mocking, later on.
领英推荐
/**
?* @description A class to provide a stub implementation for the `AccountDomain` class.
?* @author [email protected]
?* @date 02-01-2023
*/
@isTest
public class AccountStub implements System.StubProvider {
?
?/**
??* @description Handle a method call made to the stub.
??* @param stubbedObject The object the method was called on.
??* @param methodName The name of the method that was called.
??* @param returnType The return type of the method.
??* @param listOfParamTypes The list of parameter types for the method.
??* @param listOfParamNames The list of parameter names for the method.
??* @param listOfArgs The list of arguments passed to the method.
??* @return The result of the method call.
??*/
?public Object handleMethodCall(Object stubbedObject, String methodName, Type returnType, List<Type> listOfParamTypes, List<String> listOfParamNames, List<Object> listOfArgs) {
?????Object result;
?????if (methodName == 'sumWonOpps') {
???????Map<Id, Integer> integerMap = new Map<Id, Integer>();
???????for (Account acc : (List<Account>)listOfArgs[0]) {
?????????integerMap.put(acc.Id, 30);
???????}
???????result = integerMap;
?????}
?????return result;
?}
}
The AccountStub class has one public method called handleMethodCall, which is called whenever a method is called on the stubbed object.?
The handleMethodCall method takes several arguments as input, including the name of the method that was called and a list of arguments that were passed to the method. Just for the sake of this article, this method is checking that the method’s name is “sumWonOpps”, in order to always return the fixed value of 30 when implementing the Stub API mock. For this reason, this method is exactly the same as the one we’re trying to mock, since it is the method we are trying to mock in this example.
/**
?* @description A class to test the `AccountService` class.
?* @author [email protected]
?* @date 02-01-2023
?*/
@isTest
private class AccountServiceTest {
?
?/**
??* @description Initialize test data.
??*/
?@TestSetup
?static void init(){
???List<Account> accounts = new List<Account>{
?????new Account(Name='Acct1', Won_Opportunities__c=1),
?????new Account(Name='Acct2', Won_Opportunities__c=2),
?????new Account(Name='Acct3', Won_Opportunities__c=3)
???};
???insert accounts;
?}
?
?/**
??* @description Test the `summarizeData` method of the ? ? `AccountService` class.
??*/
?@isTest
?static void testSummarizeData() {
???// Prepare data
???AccountDomain accountDomainMock = (AccountDomain)Test.createStub(AccountDomain.class, new AccountStub());
?
???AccountService service = new AccountService();
???service.setAccountDomain(accountDomainMock);
?
???Map<Id, Account> accountsMap = new Map<Id, Account>(
?????[select Id from Account]
???);
?
???// Do test
???Test.startTest();
???service.summarizeData(accountsMap.values());
???Test.stopTest();
?
???// Asserts
???for (Account acc : [SELECT Id, Name, Won_Opportunities__c FROM Account WHERE Id IN :accountsMap.keySet()]) {
?????System.assertEquals(30, acc.Won_Opportunities__c);
???}
?}
}
The testSummarizeData method is used to test the summarizeData method of the AccountService class. Unlike the unit tests that we are used to, the line between test.startTest() and test.stopTest() invokes the mock we previously prepared - a mock implementation of the AccountDomain class, instead of the “real” test data that would be called on in a real unit test. Assertions are later performed to ensure proper performance.?
Best Practices
To ensure our stubbing works as efficiently as possible, here are some best practices for using the Salesforce Stub API:
Tips for troubleshooting and debugging stubs?
Since the Stub API is useful and we encourage you to start using it in your tests, find below some tips for troubleshooting and debugging this amazing API.
Summary
In conclusion, Salesforce’s Stub API is an essential tool for improving the quality and reliability of Apex code. By allowing developers to create “stub” classes that mimic the behavior of other classes or APIs, the Stub API enables developers to create more reliable, deterministic unit tests that are less prone to failures caused by external dependencies. In addition, the use of stubs can lead to faster-running tests, as they do not need to make external API calls or interact with other resources. Overall, the Stub API is an invaluable tool for ensuring the reliability and maintainability of Apex code in our CRM.?
Resources