Mocker is a mocking framework that helps you create mocks for APEX classes and interfaces. It is super easy to use and helpful for unit testing.
Unit testing is a methodology used to test individual units of source code in an application. In OOP those units are classes and their public methods.
Classes can have dependencies; in other words, classes can call other classes. To create unit tests, you must replace these dependencies with some kind of code you have control over. Those replacement classes are commonly called mocks.
Runtime dependencies Test dependencies
------------- -------------
| MyClass | | MyClass |
------------- -------------
/ \ ====> / \
/ \ / \
------------- ------------- ------------- -------------
| Dependency1 | | Dependency2 | | Mock1 | | Mock2 |
------------- ------------- ------------- -------------
To use the mocked classes created by the Mocker Framework is necessary to adapt your target classes to receive these mock instances. There are several ways to do it. Here are some suggestions.
The tested class dependencies will be received through the class constructor. We usually use this approach with some Dependency Injection framework.
public class MyService {
private IMySelector mySelector;
public MyService(IMySelector mySelector) {
this.mySelector = mySelector;
}
public void doSomething() {
// ....
List<Account> account = mySelector.getAccounts();
// ....
}
}
The class dependencies will be created using private fields. Those dependencies can be replaced by mock instances during the unit testing.
public class MyService {
@TestVisible
private IMySelector mySelector = new MySelector();
public void doSomething() {
// ....
List<Account> account = mySelector.getAccounts();
// ....
}
}
The class dependencies will be received through an IOC container. During the testing phase we can instruct the IOC container to return the mock instance instead of the real implementation.
public class MyService {
public void doSomething() {
// ....
IMySelector mySelector = Container.instance.get(IMySelector.class);
List<Account> account = mySelector.getAccounts();
// ....
}
}
The Mocker Framework works in three phases: Stubbing, Executing (Replaying) and Asserting.
- Stubbing phase: The Mocker Framework will record the mock actions and expected behaviours;
- Executing phase: It will replay the recorded actions such as mocked return values or exceptions, and it will record any action executed over the mock instance;
- Asserting phase: It will assert if the mock has executed the expected behaviours.
Given the following dependency class...
public class MyDependency {
public Contact getContactByEmail(String email) {
return [SELECT Id, NAME FROM Contact WHERE Email = :email LIMIT 1];
}
public void insertContact(Contact contact) {
insert contact;
}
}
You can mock getContactByEmail return value for specific email values...
@IsTest
public class MyTest {
@IsTest
static void myTestExample01() {
Mocker mocker = Mocker.startStubbing();
MyDependency myMock = (MyDependency) mocker.mock(MyDependency.class);
// Mocking getContactByEmail return values
mocker.when(myMock.getContactByEmail('[email protected]'))
.thenReturn(new Contact(FirstName = 'Leonardo'));
mocker.when(myMock.getContactByEmail('[email protected]'))
.thenReturn(new Contact(FirstName = 'Berardino'));
// Going to the execution phase
mocker.stopStubbing();
// Getting the mocked return values
// It will return Leonardo contact
Contact contact1 = myMock.getContactByEmail('[email protected]');
// It will return Berardino contact
Contact contact2 = myMock.getContactByEmail('[email protected]');
// It will return null
Contact contact3 = myMock.getContactByEmail('[email protected]');
}
}
Or you can mock getContactByEmail return value for any email values...
@IsTest
public class MyTest {
@IsTest
static void myTestExample01() {
Mocker mocker = Mocker.startStubbing();
MyDependency myMock = (MyDependency) mocker.mock(MyDependency.class);
// Mocking getContactByEmail return values
mocker.when(myMock.getContactByEmail('')) // It's not necessary to give an email
.withAnyValues() // Because the parameter value will be ignored
.thenReturn(new Contact(FirstName = 'Leonardo'));
// Going to the execution phase
mocker.stopStubbing();
// Getting the mocked return values
// It will return Leonardo contact
Contact contact1 = myMock.getContactByEmail('[email protected]');
// And again Leonardo contact
Contact contact2 = myMock.getContactByEmail('[email protected]');
// And again :)
Contact contact3 = myMock.getContactByEmail('[email protected]');
}
}
Or you can mix both approaches...
@IsTest
public class MyTest {
@IsTest
static void myTestExample01() {
Mocker mocker = Mocker.startStubbing();
MyDependency myMock = (MyDependency) mocker.mock(MyDependency.class);
// Mocking getContactByEmail return values
// Mocking return for Leo email
mocker.when(myMock.getContactByEmail('[email protected]'))
.thenReturn(new Contact(FirstName = 'Leonardo'));
// And mocking for any values
mocker.when(myMock.getContactByEmail(''))
.withAnyValues()
.thenReturn(new Contact(FirstName = 'Contact for any values'));
// Going to the execution phase
mocker.stopStubbing();
// Getting the mocked return values
// It will return Leonardo contact
Contact contact1 = myMock.getContactByEmail('[email protected]');
// It will return Contact for any values
Contact contact2 = myMock.getContactByEmail('[email protected]');
// It will return Contact for any values
Contact contact3 = myMock.getContactByEmail('[email protected]');
}
}
You can implement the System.Callable interface and use this class to generate dynamic values that will be returned by the mocked method.
The methodName parameter will contain the method name that was called, and the params parameter will contain the arguments passed to it during the method execution.
An example of a callable mock return:
public class GetContactCallableReturn implements Callable {
public Object call(String methodName, Map<String, Object> params) {
// Gets the email parameter.
String email = (String) params.get('email');
return new Contact(Email = email);
}
}
You can pass your callable class instance on the .thenReturn method. It will instruct the Mocker Framework to ask your callable what the return value is.
mocker.when(myMock.getContactByEmail('')) // It's not necessary to give an email
.withAnyValues() // Because the parameter value will be ignored
.thenReturn(new GetContactCallableReturn()); // Your callable mock return.
The Mocker Framework records all arguments passed to a mock method, and these data is accessible through the Mocker.MethodRecorder. It allows you to track and assert all data received by your mock class.
@IsTest
public class MyTest {
@IsTest
static void myTestExample01() {
Mocker mocker = Mocker.startStubbing();
MyDependency myMock = (MyDependency) mocker.mock(MyDependency.class);
// Mocks getContactByEmail and gets the method recorder.
Mocker.MethodRecorder getContactByEmailRec = mocker.when(myMock.getContactByEmail(''))
.withAnyValues()
.thenReturn(new Contact(FirstName = 'Contact Name'))
.getMethodRecorder(); // Allows access to the method recorder.
// Going to the execution phase
mocker.stopStubbing();
// Executes the first call with '[email protected]'
Contact contact1 = myMock.getContactByEmail('[email protected]');
// Executes the second call with '[email protected]'
Contact contact2 = myMock.getContactByEmail('[email protected]');
// Executes the third call with '[email protected]'
Contact contact3 = myMock.getContactByEmail('[email protected]');
// Asserts how many call the method getContactByEmail received
System.assertEquals(3, getContactByEmailRec.getCallsCount());
// Asserts the arguments received in each call getting
// these arguments using the parameter name.
// Asserts the first call.
System.assertEquals(
'[email protected]', getContactByEmailRec.getCallRecording(1).getArgument('email')
);
// Asserts the second call.
System.assertEquals(
'[email protected]', getContactByEmailRec.getCallRecording(2).getArgument('email')
);
// Asserts the third call.
System.assertEquals(
'[email protected]', getContactByEmailRec.getCallRecording(3).getArgument('email')
);
}
}
Similar to the return value mocking, you can mock a method to throw an exception.
You can mock getContactByEmail to throw an exception for specific email values...
@IsTest
public class MyTest {
@IsTest
static void myTestExample01() {
Mocker mocker = Mocker.startStubbing();
MyDependency myMock = (MyDependency) mocker.mock(MyDependency.class);
// Mocking getContactByEmail return value
mocker.when(myMock.getContactByEmail('[email protected]'))
.thenReturn(new Contact(FirstName = 'Leonardo'));
// And mocking getContactByEmail exception
mocker.when(myMock.getContactByEmail('[email protected]'))
.thenThrow(new MyException('Mocked exception'));
// Going to the execution phase
mocker.stopStubbing();
// Getting the mocked return values
// It will return Leonardo contact
Contact contact1 = myMock.getContactByEmail('[email protected]');
// It will return null
Contact contact2 = myMock.getContactByEmail('[email protected]');
try {
// It will throw MyException
myMock.getContactByEmail('[email protected]');
} catch(Exception e) {}
}
}
Or you can mock getContactByEmail to throw an exception for any email values...
@IsTest
public class MyTest {
@IsTest
static void myTestExample01() {
Mocker mocker = Mocker.startStubbing();
MyDependency myMock = (MyDependency) mocker.mock(MyDependency.class);
// Mocking getContactByEmail return values
// It's not necessary to set an email
mocker.when(myMock.getContactByEmail(''))
// Because the parameter value will be ignored
.withAnyValues()
.thenThrow(new MyException('Mocked exception'));
// Going to the execution phase
mocker.stopStubbing();
// Getting the mocked return values
try {
// It will throw MyException
myMock.getContactByEmail(null);
} catch(Exception e) {}
try {
// It will also throw MyException
myMock.getContactByEmail('[email protected]');
} catch(Exception e) {}
}
}
To mock void methods you will need to call the method you are mocking before the mocker.when() method calling.
@IsTest
public class MyTest {
@IsTest
static void myTestExample01() {
Mocker mocker = Mocker.startStubbing();
MyDependency myMock = (MyDependency) mocker.mock(MyDependency.class);
// Mocking getContactByEmail return values
// Calling the void method before to call the when() method
myMock.insertContact(new Contact(FirstName = 'Leo'));
// Mocking the exception
mocker.when().thenThrow(new MyException('Mocked exception'));
// Going to the execution phase
mocker.stopStubbing();
try {
// Calling the mocked method
// It will throw MyException
myMock.insertContact(new Contact(FirstName = 'Leo'));
} catch(Exception e) {}
// Calling a not recorded method (the given Contact is not Leo!)
// It will not throw an exception
myMock.insertContact(new Contact(FirstName = 'Berardino'));
}
}
In some tests we want to know if our mock class has received a method call with some parameter values or how many times a method was called. The Mocker Framework allows you to specify the expected behaviours of mocked methods. Those behaviours will be asserted when you call Mocker.assert() method.
There are four method behaviours that you can assert:
- The mocked method should be called once => .shouldBeCalledOnce();
- The mocked method should be called N times => .shouldBeCalled(Integer times);
- The mocked method should be never called => shouldNeverBeCalled();
- Mock method should be called between min and max times => shouldBeCalledBetween(Integer minTimes, Integer maxTimes);
To assert if the expected behaviour in your mock class has been executed, you will need to call the mocker.assert() method. This method compares all activities received by your mock during the execution phase with the expected ones. The assertions are made using the Salesforce System.assert operations.
If you want to be notified when an assertion fails, you can call the mocker.assert(true) method with the "true" argument. The Mocker Framework will throw an exception if an assertion fails.
@IsTest
public class MyTest {
@IsTest
static void myTestExample01() {
Mocker mocker = Mocker.startStubbing();
MyDependency myMock = (MyDependency) mocker.mock(MyDependency.class);
mocker.when(myMock.getContactByEmail('[email protected]'))
// Mocking the return value
.thenReturn(new Contact(FirstName = 'Leonardo'))
// Configuring the expected behavior
.shouldBeCalledOnce();
mocker.when(myMock.getContactByEmail('[email protected]'))
// Mocking the return value
.thenReturn(new Contact(FirstName = 'Nicolas'))
// Configuring the expected behavior
.shouldBeCalled(3);
// Going to the execution phase
mocker.stopStubbing();
// Executing the test
Contact contact = myMock.getContactByEmail('[email protected]');
// ...
// Asserts
System.asserNotEquals(null, contact);
System.asserEquals('Leonardo', contact.FirstName);
mocker.assert(); // Assert the expected mock behavior
}
}
As explained, the Mocker Framework records what is expected to be done by the mocked class (stubbing phase) and all actions executed on it (execution phase). This internal state can be cleared by calling the method mocker.clear(). All previous recorded actions and behaviour will be deleted, and your mock can be stubbed again.
The Mocker Framework also provides the MockerUtils, a utility class with useful methods to help you develop your unit test class.
You can generate Ids for your mock objects with the method generateId.
// First call generates the 001000000000001AAA Account Id
Id accountId1 = MockerUtils.generateId(Account.SObjectType);
// Second call generates the 001000000000002AAA Account Id
Id accountId2 = MockerUtils.generateId(Account.SObjectType);
// Generates the 003000000000001AAA Contact Id
Id contactId1 = MockerUtils.generateId(Contact.SObjectType);
// Generates the 003000000969132AAA Contact Id
Id contactId2 = MockerUtils.generateId(Contact.SObjectType, 969132);
Some SObjects fields, such as formula fields, roll-up summary fields or related objects list fields are read-only and cannot be set just using the equal operator. The MockerUtils provides the method updateObjectState that allows you to easily update your SObject state even if the fields are read-only.
Account accountInstance = new Account(
Id = MockerUtils.generateId(Account.SObjectType),
Name = 'Test Account',
NumberOfEmployees = 10
);
Contact contact1 = (Contact) MockerUtils.updateObjectState(
new Contact(FirstName = 'Name'), // The SObject instance to be updated
new Map<String, Object>{ // The Field Name => Data map
'Id' => MockerUtils.generateId(Contact.SObjectType),
'Name' => 'Leonardo Berardino', // Read-only field
'FirstName' => 'Leonardo', // Overrides the FirstName value
'Birthdate' => Date.today().addYears(-10) // Date field
}
);
Contact contact2 = new Contact(
Id = MockerUtils.generateId(Contact.SObjectType),
FirstName = 'Another',
LastName = 'Contact',
Email = '[email protected]'
);
Account updatedAccount = (Account) MockerUtils.updateObjectState(
accountInstance, // The SObject instance to be updated
new Map<String, Object> { // The Field Name => Data map
'Contacts' => new List<Contact> { // Sets the read-only contacts list field
contact1, contact2
},
'Website' => 'https://test.com', // Sets the Website field value
'Name' => 'New Account Name', // Overrides the Name field value
'NumberOfEmployees' => 99 // Overrides the NumberOfEmployees field
}
);
You can find several examples of unit tests using the mocking framework in the samples directory.
Use the Github Issues feature to reach out if you have feedback/questions/bugs/feature requests etc.
Please do file any issue you find, keeping the following in mind:
- Create only one issue per bug or feature request.
- Add as much information as possible and the steps to reproduce the error.
- Only add relevant comments to the issues. Many people get notified when a commentary is added to the issue, so let's keep them relevant.