resources-banner-image
Don't want to miss a thing?Subscribe to get expert insights, in-depth research, and the latest updates from our team.
Subscribe
by  SoftServe Team

Speed up your Unit Test using Stub API

clock-icon-white  7 min read

Anyone working with more than a handful of classes has probably noticed that tests can become slow and time-consuming. While overall system performance is important, developers often need practical ways to make their daily testing faster and more efficient.

To better understand the issue, let’s look at an example. A method must be created that runs before an insert event on a Contact trigger. Its main purpose is to populate a few fields based on the parent Account record.

public with sharing class PropagateAccountDataForNewContacts(){
    public void propagateAccountDataToChildContact(List<Contact> newContacts){
        Set<Id> parentAccountIds = new Set<Id>();
        for(Contact newContact : newContacts){
            parentAccountIds.add(newContact.AccountId);
        }
       Map<Id, Account> parentAccountsMap = new Map<Id, Account>
([SELECT Id, Phone, ..., 
SomeExtraField__c FROM Account WHERE Id IN :parentAccountIds]);
        for(Contact newContact : newContacts){
            setFieldsForNewContact(newContact, parentAccountsMap.get(newContact.AccountId));
        }
    }
}
 

Unit tests

Unit tests are designed to ensure error-free code by verifying that individual parts of the code work as expected. To deploy code to a production environment, at least 75% code coverage is required.

In most cases, creating a new unit test is straightforward and follows a few basic steps:

  1. Create a test class and/or test method
  2. Create test data
  3. Call the method you want to test
  4. Assert the results

This approach works well for small to medium-sized projects but often leads to problems as the codebase grows. For example, a standard unit test is simple to use:


@isTest
static void testShouldPropagateAccountDataToNewContacts(){
    Account a = TestUtil.createAccount();
    insert a;
    Contact testContact = TestUtil.createContact(a.Id);
    Test.startTest();
    insert testContact;
    Test.stopTest();

    Contact resultContact = TestUtil.getContactById(testContact.Id);
    System.assertEquals(a.Phone, resultContact.Phone,
 'Phone field should propagate from Account to Contact record.');
    System.assertEquals(a.SomeExtraField__c, resultContact.SomeExtraField__c,
 'SomeExtraField field should propagate from Account to Contact record.');
}

Unit Tests problems

Anyone with a CI setup in their projects knows how much time can be spent on production deployment. The execution time of unit tests becomes a bigger issue in larger teams, especially when database usage is high.

In Salesforce, creating true unit tests is not simple. Following the standard approach often results in service tests instead of unit tests. The main purpose of these tests is to check end-to-end logic without breaking the code into smaller pieces. To create actual unit tests and reduce test execution time, a different approach is needed.

In the example provided, there is no problem with the test itself — unless it is run in an org with 154 declarative tools working on the Contact object.

New Approach

To create unit tests that can truly be considered real unit tests, a data access layer must be built. This layer communicates with the database only when necessary and allows test execution to run entirely in memory. In implementing this approach, several key questions arise:

  1. How can test data be created without using DML operations?
  2. What if SOQL queries are included in the logic?
  3. How will this extra layer affect a developer’s work and the maintenance of the code?

One of the most critical fields on a record is the ID, which is a primary reason DML operations are typically used during test setup. However, a database is not required to provide an ID when building proper unit tests — a record can be created with an ID directly.

To generate a record ID, a unique prefix for the SObject is required; otherwise, an error will occur. The Schema class method must be used to obtain the correct prefix:

‘sObjectType.getDescribe().getKeyPrefix()’, where sObjectType can be taken from a global description or from SObject class itself – ‘Account.SObjectType’. Simple function that can be used for this purpose:


public static String getFakeId(Schema.SObjectType sObjectType){
        String result = String.valueOf(fakeIdNumber++);
        return sObjectType.getDescribe().getKeyPrefix() + '0'.repeat(12-result.length())
 + result;
    }

Once a record is created with an ID, that ID can be used in lookup or master-detail relationships to build the data structures needed for the test.

Any SOQL queries included in the logic should be moved into a separate class, known as the Data Access Layer. These classes become the primary point of contact with the database. For example, instead of:


for(Contact contact : [SELECT Id, Name FROM Contact WHERE Id IN :contactIds]){
    Dostuff(contact);
}

A Data access layer class can be structured as follows:


for(Contact contact : contactDataAccessLayerClass.getContactsForGivenIds(contactIds)){
    Dostuff(contact);
}

All this extra layer provides significant value. First, it is easier to maintain a single point of contact with the database than to have queries scattered throughout the code. Descriptive methods for querying data can be created, making it easier for new developers to understand and work with the code in the future. Finally, the results of a query can be mocked in unit tests, allowing the business logic to be tested thoroughly without heavily impacting resource usage.

In the example provided, a simple class can be implemented like this:


public with sharing class AccountDataAccess {
    public List<Account> getAccountsForGivenIds(Set<Id> accountIds){
        if(accountIds.isEmpty()){
            return [SELECT Id, Phone, ...,
 SomeExtraField__c FROM Account WHERE Id IN :accountIds];
        }
            else
       {
            return new List<Account>();
        }
    }
}

To speed up unit test execution using a stub API, a mocking framework must be built to simulate the database. Some tests may already use this mocking technique, as it is a standard approach for testing code that relies on callouts. The concept is similar: mock results from external systems are used to verify that the business logic produces the expected outcomes. In this case, the “external system” is the database.

To prepare for this, a few classes are created. The first is MockProvider, which allows users to mock all methods within a mocking class by passing a map.


@isTest
public class MockProvider implements System.StubProvider {
    private Map<String, Object> stubbedMethodMap;

    public MockProvider(Map<String, Object> stubbedMethodMap) {
        this.stubbedMethodMap = stubbedMethodMap;
    }

    public Object handleMethodCall(Object stubbedObject, String stubbedMethodName,
 Type returnType, List<Type> listOfParamTypes,
 List<String> listOfParamNames, List<Object> listOfArgs) {
        Object result;
        if (stubbedMethodMap.containsKey(stubbedMethodName)) {
            result = stubbedMethodMap.get(stubbedMethodName);
        }
        return result;
    }
}

The next step is to create a MockService class, which will be responsible for mocking the results of functions, as shown below:


public class MockService {
    private MockService() {}

    public static MockProvider getInstance(Map<String, Object> stubbedMethodMap) {
        return new MockProvider (stubbedMethodMap);
    }
    
    public static Object createMock(Type typeToMock, Map<String, Object> stubbedMethodMap) {
        return Test.createStub(typeToMock, MockService.getInstance(stubbedMethodMap));
    }
}

As shown, the createMock function is used to create a mock. By using Test.createStub, the Stub API is invoked, informing the system which class will be mocked and what should be returned for specific method calls. Using this service is very straightforward:


ClassToTest.dataAccessLayerClassInstance = (DataAccessLayerClass)
MockService.createMock(DataAccessLayerClass.class, new Map<String, Object>{
    'getRecords' => resultForGetRecords,
    'getChildRecords' => resultForGetChildRecords,
    'updateRecords' => null
});

Once ‘getRecords()’ method from ‘dataAccessLayerClassInstance’ is called within tested class, result will be ‘resultForGetRecords’.

In the example mentioned, a few small adjustments are needed to use the Stub API in tests. As noted earlier, the Data Access Layer class should be used instead of inline SOQL queries.


public with sharing class PropagateAccountDataForNewContacts(){
    @TestVisible private AccountDataAccess accountData = new AccountDataAccess();
    
    public void propagateAccountDataToChildContact(List<Contact> newContacts){
        Set<Id> parentAccountIds = new Set<Id>();
        for(Contact newContact : newContacts){
            parentAccountIds.add(newContact.AccountId);
        }
        Map<Id, Account> parentAccountsMap = new Map<Id, Account>
(accountData.getAccountsForGivenIds(parentAccountIds));
        for(Contact newContact : newContacts){
            setFieldsForNewContact(newContact, parentAccountsMap.get(newContact.AccountId));
        }
    }
}

Once these changes are in place, you simply use the MockService class within the unit test, allowing tests to run quickly without interacting with the database.


@isTest
static void testShouldPropagateAccountDataToNewContacts(){
    Account mockedAccount = new Account(
        Id = TestUtil.getFakeId(Account.SObjectType),
        Name = 'Acme',
        Phone = TestUtil.generatePhone(),
        SomeExtraField__c = 'extraValue'
    );
    Contact testContact = new Contact(
        Id = TestUtil.getFakeId(Contact.SObjectType);
        FirstName = 'John',
        LastName = 'Doe'
    );
   PropagateAccountDataForNewContacts testedClass = new PropagateAccountDataForNewContacts();
    testedClass.accountData = (AccountDataAccess) MockService.createMock
(AccountDataAccess.class, new Map<String, Object>
{
        'getAccountsForGivenIds' => new List<Account>{mockedAccount}
    });

    Test.startTest();
        PropagateAccountDataForNewContacts.propagateAccountDataToChildContact
        (new List<Contact>{testContact};
    Test.stopTest();

    System.assertEquals(mockedAccount.Phone, testedClass.Phone, 
   'Phone field should propagate from Account to Contact record.');
    System.assertEquals(mockedAccount.SomeExtraField__c, testedClass.SomeExtraField__c,
   'SomeExtraField field should propagate from Account to Contact record.');
}

Gain support to move faster. Our SFDC team of Stub API experts understands the full software development ecosystem and can help design and implement a clear strategy to build a mocking framework with the Stub API for your business.

Start a conversation with us