Apex Mocking Framework & Unit Tests
Software development is much more than writing some lines of code to bring to life an idea to use by hundreds, thousands, if not millions of people: planning the development process, designing the software architecture, properly testing the solution, and deploying it in production are several phases of software development that are invisible to the final user yet crucial for delivering a good product. We have different tools to support our work in each stage of software development, which is also true for application tests. Groundswell Cloud Solutions has created an open source, no dependencies, and easy-to-use APEX mocking framework that can help you quickly make the assets necessary for your application unit test development.
Software Tests
An application needs to have a sound testing process. The tests ensure that we deliver a high-quality product to our clients with fewer bugs and adhere to the project requirements. Application tests also give us more confidence for future improvements because they will show us if we have broken a feature while implementing new functionalities.
There are several tests, each covering different aspects of the software, some automated and others not. The following list shows some of them:
- Unit Tests
- Integration Tests
- Functional Tests
- End-to-end Tests
- Acceptance Tests
- UI Tests
- Performance Tests
- Smoke Tests
- Security Tests
Complex tests, such as functional tests, will take more time to run, while simple tests, like unit tests, take less time. This is why it is so important to have a good testing plan to determine the order of which test types to execute and allocate them efficiently for a good balance between coverage and performance.
Integration Tests
The integration tests are responsible for testing if different modules or services from the application work well together and evaluating the system’s compliance with specified functional requirements.
Usually, high-level integration tests are implemented to test the entry points of the software, such as controllers, API, event handlers, etc. All software internals are hidden from these tests, meaning they will only test what the ‘external world’ can see.
The following diagram shows what is happening internally in the application while the integration tests are running, why they are heavy, and why they take so long to run.
Another aspect of the integration tests in Salesforce is data generation. These simulate actual operations from the client or system integration perspective that validate database data requirements and functionality.
Salesforce provides us with an empty test database while the test is running and the @TestSetup annotation inside the test classes where we can prepare the necessary data for the test. This creates another difficulty – generating before running the test, the test records, and their dependencies, without violating the database and validation rules constraints. It also costs time because the database is a very slow resource compared to memory.
Unit Tests
Unit tests are low-level and close to the application source code tests. They test individual methods and functions of the classes, components, or modules, isolating them from their dependencies. They also will use data in memory and will not access any external resource or other internal layers of the application, which will reflect in extreme fast tests.
A crucial characteristic of unit testing is the ability to replace the real dependencies instances with objects we can control and manipulate to simulate the behavior of the real objects. These replacing objects are usually called Mocks.
Salesforce provides a Stub API that allows us to create Mocks that can be used to replace the real dependencies of a tested class. However, this API is not so easy to use and requires implementation of the System.StubProvider interface.
The lack of a better tool to help create unit tests motivated us to develop the GS APEX Mocking Framework, an easy-to-use, with no dependencies framework that hides the complexities of working with the Salesforce Stub API.
Preparing Your Code
To properly create a unit test, we need to access the tested class dependencies and replace them with mocked classes. But how can we do it?
We can use several techniques for decoupling our classes, the most common being the IOC containers with Dependency Injection. Luckily, Salesforce has the @TestVisible annotation, and we can use it in this context, avoiding the necessity of fancy and complex frameworks.
The first step is not to use static methods in important system classes. Utilizing the instance classes is essential because the Salesforce Stub API does not support static methods stubbing. We can leave static methods for the utility classes.
public inherited sharing class PaymentService {
// Here we can remove the static keyword
public static String processPayment(Id invoiceId) {
// Process the payment
}
}
The second is moving the dependencies to class fields annotated with the @TestVisible annotation.
public with sharing class PaymentController {
@TestVisible
private static PaymentService paymentService = new PaymentService();
@AuraEnabled
public static String processPayment(Id invoiceId) {
try {
paymentService.processPayment(invoiceId);
} catch (Exception e) {
throw new AuraHandledException(e.getMessage());
}
}
}
And there you have it; we are ready to create unit tests.
The GS APEX Mocking Framework
The GS APEX Mocking Framework is an open source, no dependencies, and easy-to-use mocking framework that encapsulates the complexities of using the Salesforce Stub API, delivering a bunch of functionalities that will help you to create your application unit tests.
- It encapsulates the Salesforce Stub API complexity exposing an easy-to-use fluent interface.
- It records the expected mock behavior and replicates this behavior during the test execution phase.
- It also collects the execution statistics, such as argument values, number of calls, etc., that can be asserted in the Asserting phase.
- It provides a utility class to help you create Salesforce IDs to simulate real database data or to set read-only SObjects field values such as formula fields, lookup lists, rollup summary fields, etc.
The Three Test Phases
The mocking framework works in three phases: Stubbing, Executing (Replaying), and Asserting phases.
- Stubbing phase: The mocking framework records the expected mock behaviors.
- Executing phase: It replays the recorded mock object behaviors and records the execution information, such as the number of calls a method has received, the values of the arguments, etc.
- Asserting phase: It asserts if the class under test has executed the expected behavior over the mocked dependencies.
Mocking Your Dependencies Behaviour
A key feature of the framework is the ability to mimic the exact behavior we want during the execution phase. We can define expected return values, exceptions, and other behaviors, allowing us to control our tests fully.
Let’s use the following class as an example: PaymentService is the class under test.
public class PaymentService {
@TestVisible
private static InvoiceSelector invoiceSelector = new InvoiceSelector();
public static String processPayment(Id invoiceId) {
Invoice__c invoice = invoiceSelector.getInvoiceById(invoiceId);
if(invoice == null) {
throw new PaymentException('Invoice not found');
}
// Process payment code
// ...
}
}
And its dependency:
public inherited sharing class InvoiceSelector {
public Invoice__c getInvoiceById(Id invoiceId) {
List<Invoice__c> invoices = [
SELECT Id, Name, TotalAmount
FROM Invoice__c
WHERE Id = :invoiceId
LIMIT 1
];
return invoices.isEmpty() ? null : invoices[0];
}
}
Now we are ready to start creating our unit test.
Implementing the Unit Test
The first step in our test method is to prepare the data that will be used to simulate the database access. The MockerUtils.generateId helps us with the Salesforce SObjects IDs creation.
@IsTest
public class PaymentServiceTest {
@IsTest
static void processPaymentShouldCallBrokerWhenInvoiceIsValid() {
// 1) Generating a fake Invoice Id and creating the Invoice in memory
Id invoiceId = MockerUtils.generateId(Invoice__c.SObjectType);
Invoice__c invoice = new Invoice__c(
Id = invoiceId, State__c = 'Open', /* Other fields.*/
);
Now, that we have the Invoice instance, we will use it to simulate the database data. We can start to record the behaviour we want in our mocked dependency classes (InvoiceSelector and PaymentBrokerClient). Calling the Mocker.startStubbing method will instruct the framework to record the behaviors.
// 2) Starting the mocks behavior recording phase
Mocker mocker = Mocker.startStubbing();
Next, we are mocking the InvoiceSelector and recording the return for the getInvoice method, which will return the invoice we created in step 1 if the invoice ID matches the generated ID. If not, the method will return null.
// 3) Creating the InvoiceSelector mock instance
InvoiceSelector selectorMock = (InvoiceSelector)
mocker.mock(InvoiceSelector.class);
// and recording the getInvoice method behavior
mocker.when(selectorMock.getInvoice(invoiceId)).thenReturn(invoice);
We also are mocking the PaymentBrokerClient, but in this case, we are getting the method recorder to assert if the PaymentService has called the client correctly. We are not testing what the real PaymentBrokerClient is doing because this is the responsibility of its unit test.
One note in this step is we are calling the PaymentBrokerClient.processPayment() right before the then() method instead of inside it because the processPayment() method is returning void.
// 4) Creating the Payment Broker Client mock instance
PaymentBrokerClient brokerClientMock = (PaymentBrokerClient)
mocker.mock(PaymentBrokerClient.class);
// and getting the method recorder
brokerClientMock.processPayment(invoiceId);
Mocker.MethodRecorder processPaymentRec = mocker.when().getMethodRecorder();
At this point, we have our mocks and data ready. We can stop the recording phase and replace the PaymentService dependencies with our mocks.
// 5) Stopping the recording phase and going to the execution phase
mocker.stopStubbing();
// 6) Replacing the dependencies with the mocked classes
PaymentService.invoiceSelector = selectorMock;
PaymentService.brokerClient = brokerClientMock;
We are ready for running the method under test.
// 7) Running the test
Test.startTest();
new PaymentService().processPayment(invoiceId);
Test.stopTest();
The expected flow on the PaymentService().processPayment() method is:
- Call the selector to get the invoice with the given invoice ID
- Call the client broker once passing the invoice from the step 1
And this is exactly what we are asserting. Using the method recorder from step 4, we have access to everything that happened with the PaymentBrokerClient.processPayment() method. For example, we can access how many calls and arguments it has received.
// 8) Asserting how many calls the method
// PaymentBrokerClient.processPayment(Invoice__c invoice) has received
System.assertEquals(1, processPaymentRec.getCallsCount());
// 9) Asserting the argument invoice from the
// PaymentBrokerClient.processPayment(Invoice__c invoice) method call
System.assertEquals(
invoice, processPaymentRec.getCallRecording(1).getArgument('invoice')
);
Here we have the complete unit test code:
@IsTest
public class PaymentServiceTest {
@IsTest
static void processPaymentShouldCallBrokerWhenInvoiceIsValid() {
// 1) Generating a fake Invoice Id and creating the Invoice in memory
Id invoiceId = MockerUtils.generateId(Invoice__c.SObjectType);
Invoice__c invoice = new Invoice__c(
Id = invoiceId, State__c = 'Open', /* Other fields.*/
);
// 2) Starting the mocks behavior recording phase
Mocker mocker = Mocker.startStubbing();
// 3) Creating the InvoiceSelector mock instance
InvoiceSelector selectorMock = (InvoiceSelector)
mocker.mock(InvoiceSelector.class);
// and recording the getInvoice method behavior
mocker.when(selectorMock.getInvoice(invoiceId)).thenReturn(invoice);
// 4) Creating the Payment Broker Client mock instance
PaymentBrokerClient brokerClientMock = (PaymentBrokerClient)
mocker.mock(PaymentBrokerClient.class);
// and getting the method recorder
brokerClientMock.processPayment(invoiceId);
Mocker.MethodRecorder processPaymentRec = mocker.when().getMethodRecorder();
// 5) Stopping the recording phase and going to the execution phase
mocker.stopStubbing();
// 6) Replacing the dependencies by the mocked classes
PaymentService.invoiceSelector = selectorMock;
PaymentService.brokerClient = brokerClientMock;
// 7) Running the test
Test.startTest();
new PaymentService().processPayment(invoiceId);
Test.stopTest();
// 8) Asserts how many calls the method
// PaymentBrokerClient.processPayment(Invoice__c invoice) has received
System.assertEquals(1, processPaymentRec.getCallsCount());
// 9) Asserts the argument invoice from the
// PaymentBrokerClient.processPayment(Invoice__c invoice) method call
System.assertEquals(
invoice, processPaymentRec.getCallRecording(1).getArgument('invoice')
);
}
}
Other Features
The GS Apex Mocking Framework has many other features to help our unit test development. Please, access the project’s GitHub page for more information.
// It can have different return values depending on the argument value
mocker.when(selectorMock.getInvoice('0I1000000111AAA'))
.thenReturn(new Invoice__c(Amount__c = 1000.0));
mocker.when(selectorMock.getInvoice('0I1000000222AAA'))
.thenReturn(new Invoice__c(Amount__c = 2000.0));
// And it can ignore the other argument values and always return the same Invoice
mocker.when(selectorMock.getInvoice(null))
.withAnyValues()
.thenReturn(new Invoice__c(Amount__c = 3000.0));
// It can also simulate an exception
mocker.when(selectorMock.getInvoice('0I1000000333AAA'))
.thenThrow(new DmlException('Record not found'));
// We can define expected behaviors
mocker.when(selectorMock.getInvoice('0I1000000444AAA'))
.thenReturn(new Invoice__c(Amount__c = 4000.0))
.shouldBeCalledOnce();
mocker.when(selectorMock.getInvoice('0I1000000555AAA'))
.shouldNeverBeCalled();
// We can set readonly SObjects fields, such as formula fields,
// related lists fields, roll-up summary fields, etc.
Contact contact1 = (Contact) MockerUtils.updateObjectState(
new Contact(FirstName = 'Name'), // The SObject instance
new Map<String, Object>{ // The Field Name => Data map
'Id' => MockerUtils.generateId(Contact.SObjectType),
'Name' => 'John Smith', // Read-only field
'FirstName' => 'John', // Overrides the FirstName value
'Birthdate' => Date.today().addYears(-10) // Date field
}
);
Conclusion
Balancing the number of unit and integration tests we implement in a project can drastically increase the coverage and decrease the time to execute them. We are excited to share our GS Apex Mocking Framework with the wider Salesforce community, and help you accelerate your unit test development in your organizations.