Anybody that has worked with a code base bigger than ten classes has come across challenges relating to the execution time of Unit Tests. While the overall system’s performance must improve, this blog post will aim to provide an answer to what a developer can do to speed up his or her work.
To gain better insight into the issue, let’s take a look at an example. A method must be created that will be invoked on before an insert event on a Contact object trigger, and it’s main purpose is to set a couple of 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 supposed to ensure an error-free code, by verifying that parts of a code are working without any issues. To deploy code to the production environment, at least 75 percent of code coverage should be reached using Unit Tests. Most of the time, the process of creating a new Unit Test is quite simple, as shown in the following steps:
- Create test class and /or test method
- Create test data
- Call method that we want to test
- Assert results
This approach works well when managing small and middle-sized projects, but can cause many problems once the code base grows. For example, the standard Unit Test is quite simple to utilize:
@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 that has a CI set up in their projects is well and truly aware how much time is spent on production deployment. The time execution required for Unit Tests starts to become a problem, especially with bigger teams because of a high usage of the database.
In Salesforce, creating Unit Tests that are actually Unit Tests is not an easy and simple task. When a standard approach is followed, instead of Unit Tests theService Tests are created. Those main purope is to check end-to-end logic without breaking the code into many smaller pieces. To create actual Unit Tests, and to reduce test execution time we must change the thought process. There is no problem with test in the provided example, unless this test is executed on org with 154 Declarative tools that are working on Contact object.
New Approach
To enable Unit Tests to be considered as real Unit Tests, a data access layer must be built to communicate with the database only and initiate a new way of test execution—all in memory. In doing so, the following questions are required:
- How can we build a test data without using DMLs?
- What if SOQLs are included in the logic?
- How this extra layer will affect a developers work, and the maintenance of code?
The most critical field on a record is the ID, and it’s one of the key reasons why we use DMLs during the test setup. For building proper Unit Tests a database is not required to provide a record with an ID. Instead a record with an ID can be created.
To create a record ID, a unique prefix of an SObject is required, otherwise an error will be generated. To receive a proper prefix, the Schema class method must be used:
‘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, then the ID can be used in a lookup or master-detail relation to create data structures as required for the test.
Any SOQLs included in the logic should be extracted into a separate class, which will be known as the Data Access Layer. These type of classes will beourpoint of contact withthe database. For example instead of:
for(Contact contact : [SELECT Id, Name FROM Contact WHERE Id IN :contactIds]){
Dostuff(contact);
}
A Data Access Layer class as follows can be used:
for(Contact contact : contactDataAccessLayerClass.getContactsForGivenIds(contactIds)){
Dostuff(contact);
}
All this extra layer does is provide an extra value. Firstly, it’s easier to maintain a single point of contact with the database than having queries all over the code. Descriptive methods that will query data can be facilitated, making the learning curve easier to adapt for people working on this code in the future. Finally, there is the possibility to mock results of a query in Unit Tests, so that the business logic can be tested from top to bottom without making a heavy impact on the usage of resources. In the example provided, a simple class can be applied, 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>();
}
}
}
Stub API
To speed up a Unit Tests execution using stub API, a mocking framework must be built to ‘mock’ a database. There may have already been some tests using mocking results technique because it’s a standard way of testing code that is using callouts. The overall concept is somewhat similar: mock results of other external systems are used to check expected results against the business logic. In this scenario, the external system is the database. As a preparation, a couple of classes are created. The first one will be MockProvider: this class 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 that will be responsible for mocking results of functions, as follows:
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 can be seen the function createMock is used for creating a mock. By using Test.createStub the Stub API is invoked—the system is informed which class is going to be ‘mocked’, as well as what should be returned for particular methods calls. Usage of this service is very simple:
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 mentioned example, some small adjustments must be done in order to use Stub API in tests. As mentioned above, Data Access Layer class must be used instead of an inline SOQLs.
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 the changes are in place, all that is required is to use the MockService class within the Unit Test class and have quick results, without touching 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 quickly—our SFDC team consisting of Stub API experts understand the full spectrum of the software coding ecosystem and can help to design and execute a clear strategy to build a mocking framework with the Stub API for your business. Contact SoftServe today.