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

  • You can streamline and improve testing in order to help you create faster, more reliable tests.
  • You can use it to test classes in isolation, which is important for unit testing.
  • If you decide to build your mocking framework with the stub API, it can be beneficial due to the fact that objects are generated at runtime.
  • Because these objects are generated dynamically, you don’t have to package and deploy test classes.
  • Speed – since the Stub API does not make API calls or create test data in the database, which ultimately is much faster.
  • Keep your production code free of unsightly Test.isRunningTest() statements.

Limitations

Not everything can be great! The Stub API has a few limitations, listed below:

  • 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.
  • You can’t mock the following Apex elements: static (including future methods) and private methods, properties, triggers, inner classes, system types, and classes that implement the Batchable interface and that have only private constructors.
  • The Stub API does not support asynchronous operations or long-running processes.
  • Iterators can’t be used as return types or parameter types.

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:

  • Define the behavior 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.

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 miquel@heroforge.tech  
  * @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 miquel@heroforge.tech  
  * @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 miquel@heroforge.tech
 * @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 miquel@heroforge.tech
 * @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:

  • Use stubs to create more reliable, deterministic tests: By mocking the behavior of external dependencies, you can create tests that are less prone to failures caused by external factors.
  • Use stubs to create faster-running tests: By eliminating the need to make external API calls or interact with other resources, you can create tests that run faster and can be executed more frequently.
  • Use stubs to test code in isolation: By using stubs, you can test the behavior of individual components of your code without being affected by the behavior of other components.
  • Use stubs to reduce the complexity of your tests: By eliminating the need to set up complex test data or interact with external dependencies, you can create simpler, more focused tests that are easier to maintain.
  • Use stubs to improve test coverage: By using stubs, you can create tests that cover more scenarios and edge cases, improving the overall coverage of your code
  • Use stubs in combination with other testing tools: The Stub API is just one of the many tools available to developers for creating unit tests. By combining the use of stubs with other testing tools, you can create more comprehensive and effective tests.
  • Use stubs in combination with continuous integration and delivery (CI/CD) practices: By integrating the use of stubs into your CI/CD pipeline, you can ensure that your code is thoroughly tested and ready for deployment.

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.

  • Check your debug logs: if your stub is not behaving as expected, you can use the debug log to track the method calls made to your stub. This can help you identify any issues with your stub implementation or the way it is being called.
  • Use the System.assert() method: you can use the System.assert() method to validate the results of your stubbed methods and ensure that they are returning the expected values. This can help you catch any errors or discrepancies in your stub implementation.
  • Debug stubs in production environments: if you’re having trouble debugging a stub in a production environment, you can use the System.test.setStubbing() method to enable stubbing in a production environment, but be sure to turn off stubbing once you are finished.
  • Keep your stubs concise and focused: complex or overly general stubs can be difficult to troubleshoot and maintain. It can be helpful to keep your stubs concise and focused on a specific task, rather than trying to cover every possible scenario.
  • Use the handleMethodCall() method to your advantage: said method is the primary method for implementing the logic of your stub. You can use it to add debug statements or conditional logic to your stubs to help you understand how they are being used.
  • Do not forget to reset your stubs: after running a test method that uses a stub, be sure to reset your stubs by calling the System.test.reset() method. This will ensure that your stubs are in a known state for the next test method.

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

https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_interface_System_StubProvider.htm#apex_System_StubProvider_handleMethodCall

https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_interface_System_StubProvider.htm

https://trailhead.salesforce.com/es/content/learn/modules/unit-testing-on-the-lightning-platform/mock-stub-objects

https://codingwiththeforce.com/tag/stub-api/

https://www.youtube.com/watch?v=5pjfOhwKkdE

No Comments

Sorry, the comment form is closed at this time.