Many Apex unit tests are more like integration tests than unit tests. How many times do the unit tests you’ve written use DML and SOQL on custom objects? Strictly speaking, such a unit test is an integration test as it relies on the database to pass. A good unit test should use dependencies, such as the database, that are abstracted. In that way it can exercise purely the functionality of the class under test. The abstracted dependencies should be mocked to provide the expected results for the test.
See the reference Apex docs for the API. It is suggested that you keep a page open with the Apex docs loaded for your reference whilst reading this page.
If a failure occurs in the MockerV1 code, an AssertionException is thrown. See Apex docs.
If you wish to try the Mocker example code, see Geting Started.
For an example of a complete suite of unit tests written using MockerV1 that test their features in isolation, see Asynhcronous Unit Tests
To mock a class or interface, first call the MockerV1.of method.
The argument to the method can be either the Type to be mocked, or a MockerV1.Factory instance.
To create the mocked instance of the clsss or interface, call the mock method on the MockerV1 returned by the of call.
If of is called with the Type argument then a mocked instance of that Type will be created.
If of is called with a Factory argument, then the Factory.stub method is called. The implementation of this method must create a stub for the class to be mocked by calling the Test.createStub method and setting the stubProvider to the argument passed to the stub method. Why is this necessary? It’s because of namespace boundaries. If you are using the Force Framework package, then it will not be possible to pass any of the Types in your local namespace to MockerV1.of unless they are global. Instead, you can pass a public Factory implementation which creates the stub for the class to be mocked. This works because the Factory interface is global and so accessible both in your local namespace and also the package’s.
See the difference between the MockerV1.of methods in the example unit tests. The Source Example shows how to use MockerV1 when working in an org with the source code deployed. The Package Example shows the same tests when using an org with the package deployed.
When using an org with the source code deployed, the MockerTestAPI interface is in the same namespace as the MockerV1 class and can be passed to the of method.
When using an org with the package deployed, the MockerTestAPI interface is in a different namespace to the Mocker class, and unless global, cannot be passed to the MockerV1.of method. To prevent having to make the API interface global, the unit test uses the MockerFactory class to create the stub in its own namespace.
After creating the MockerV1, you can start adding the method arguments, your mocked object is expecting. Take the interface from the example code.
public interface MockerTestAPI {
Datetime getCurrentTime();
void setCurrentTime(Datetime currentTime);
String getOrganizationId();
List<Account> addAccounts(List<Account> accounts);
}
Inspecting the interface, you will see there are three distinct method arguments. No arguments, Datetime, and
_List
mocker
.whenNoArguments()
.forMethod('getCurrentTime')
.returns(Datetime.now())
.forMethod('getOrganization')
.returns (new Organization(Id = MockerV1.fakeId(Organization.SObjectType)))
.whenArgument(Datetime.now())
.forMethod('setCurrentTime')
.whenArgument(new List<Account> { new Account(Name = 'Bill'), new Account(Name = 'Ted') })
.forMethod('addAccounts')
The arguments passed to the mocked method when called from a unit test, must exactly match those set as the expected arguments in the whenArgument(s) call. If the arguments don’t match, any methods linked to those arguments will not have their call count incremented or any of the additional logic associated with the method performed.
The returns method defines the return value from the mocked method.
The mocked method can be made to throw an exception instead.
.whenArgument(Datetime.now())
.forMethod('setCurrentTime')
.throws(new MockerV1.APIException('Example exception')
We can set the number of times the mocked method is expected to be called when the unit test is run. Add the called method after forMethod to set the number of times the method should be called.
.whenArgument(Datetime.now())
.forMethod('setCurrentTime')
.called(1)
At the end of the unit test call MockerV1.validate to check that all the mocked methods have been called the expected number of times. If the method has not had the expected number of calls set, it may be called any number of times, including zero.
By default, all argument matching, except for SObjects and Exceptions, is literal. From the previous example, whenArgument(Datetime.now()) would not be matched unless the class using the mocked method passed exactly the same Datetime value as when the argument was added to the mocked object. This is highly unlikely and argument comparators can be used to get round this issue.
MockerV1 includes a default argument comparator for the Exception type. The argument comparator checks that the Type of the Exception and its message matches that specified in the expected argument for the mocked method. This means that the stack trace is not checked. A literal comparison on two Exceptions would expect the stack trace to match too.
The default argument comparator for SObject checks that the argument passed to the mocked method is of the same object type as the expected argument for the mocked method. And that the expected argument has field values which are a sub-set of the argument passed to the mocked method. This matching logic is also applied to arguments which are Lists and Sets of SObjects.
Take the following example.
Account1 ( Name = ‘Bill’, BillingCountry = ‘UK’ ) Account2 ( Name = ‘Ted’, BillingCountry = ‘UK’ )
Take the following MockerV1.
.whenArgument(new List<Account> { new Account(BillingCountry ='UK'), new Account(BillingCountry = 'UK') })
.forMethod('addAccounts')
As the expected argument contains only BillingCountry, both Account1 and Account2 would match if passed as the List of Accounts to the mocked method.
Now consider the following MockerV1.
.whenArgument(new List<Account> { new Account(Name='Bill', BillingCountry = 'UK'), new Account(Name = 'Ted', BillingCountry = 'US') }))
.forMethod('addAccounts')
The expected argument has Accounts with Name and BillingCountry set. If the List of Accounts passed to the mocked method was Account1 and Account2, Account2 would not match as it has a BillingCountry of US and the expected argument for the second List entry has a BillingCountry of US.
Returning to the Datetime argument issue. We can address the problem by adding a custom argument comparator for the argument. Any custom argument comparator will be called before the default argument comparators, so if you wanted to add your own Exception or SObject argument comparator, you can.
The first thing you need to do is write your own argument comparator implementation for the argument. Here’s the one used in the example code.
private with sharing class DatetimeComparator implements Comparator<Object> {
public Integer compare(Object param1, Object param2) {
return (param1 instanceof Datetime && param2 instanceof Datetime) ? 0 : -1;
}
}
Note that the argument comparator is simply checking that the argument passed to the mocked method and its expected argument are of the same Type. The value of the argument is ignored.
The MockerV1 argument definition becomes.
.whenArgument(Datetime.now())
.forMethod('setCurrentTime')
.withComparators(new List<Comparator<Object>> { new DatetimeComparator() })
.called(1)
Adding the argument comparator for Datetime took some extra development effort. It’s also not something you’d want to repeat for all the other arguments your methods may take that you don’t care about the value of when mocking it.
This is where MockerV1.any can be used instead. The following MockerV1 argument definition will add a method argument of type Datetime which can take any value, including null, when the mocked method is matched.
.whenArgument(MockerV1.any(Datetime.class))
.forMethod('setCurrentTime')
.called(1)
There may be times when you want your mocked object to return a value from a method based on the arguments passed to the mocked method. MockerV1 supports this with the MockerV1.Modifier interface. If you set the returns argument of a forMethod definition, the modifier’s process method is called, with the arguments passed to the mocked method.
The following from the example code shows how a fake object id can be added to a List of Account objects using a Modifier.
.whenArgument(new List<Account> { new Account(Name = 'Bill') })
.forMethod('addAccounts')
.called(1)
.returns(new AccountModifier())
private with sharing class AccountModifier implements MockerV1.Modifier {
public Object process(List<Object> arguments) {
List<Account> returnList = new List<Account>();
for (Account acc : (List<Account>) arguments[0]) {
Account copy = acc.clone();
copy.Id = MockerV1.fakeId(Account.SObjectType);
returnList.add(copy);
}
return returnList;
}
}
The MockerV1 class is designed to allow the mocking of an object to be defined as a single statement. The MockerV1 used to test the API when used by a service in the example code shows this.
MockerTestAPI mockedAPI = (MockerTestAPI) MockerV1.of(MockerTestAPI.class)
.whenArgument(new List<Account> { new Account(Name = 'Bill') })
.forMethod('addAccounts')
.called(1)
.returns(new AccountModifier())
.whenArgument(new List<Account> { new Account(BillingCountry = 'UK'), new Account(BillingCountry = 'UK') })
.forMethod('addAccounts')
.called(2)
.returns(new AccountModifier())
.whenNoArguments()
.forMethod('getCurrentTime')
.called(5)
.returns(Datetime.now())
.mock();