This project provide a simple, lightweight, easy to read, fully tested mocking library for apex built using the Apex Stub API. We want its usage to be simple, its maintainability to be easy and to provide the best developer experience possible
Table of Contents
APIs design come from our experience with Mockito, chai.js, sinon.js and jest. The library aims to provide developers a simple way to stub, mock, spy and assert their implementations. Dependency Injection and Inversion Of Control are key architectural concepts the system under test should implements
It helps you isolate the code from its dependency in unit test. Using the library to mock your classes dependencies will contribute to improve code quality and maintanibility of your project.
It helps you write unit test by driving the behavior of the class dependencies (instead of relying on it by integration). Using the library to mock DML and SOQL from your tests will help you save a lot of time in apex test execution (the tests will not sollicitate the database anymore).
Deploy via the deploy button
Or copy force-app/src/classes
apex classes in your sfdx project to deploy it with your favourite deployment methods
Or you can install the library using our unlocked package without namespace from the latest release
It's not possible to install a non namespaced unlocked package into a namespaced org.
In this case you have those choices:
- Install from sources (with or without manually prefixing classes)
- Create your own unlocked/2GP package with your namespace containing the sources
It's not recommended for a 2GP package to depends on an unlocked package, namespaced or not (ISV scenario).
To mock an instance, use the Mock.forType
method
It returns a Mock instance containing the stub and all the mechanism to spy/configure/assert
Mock myMock = Mock.forType(MyType.class);
Because Test.createStub()
call cannot cross namespace, we provide a StubBuilder
interface to stub type from your namespace.
Create a StubBuilder
implementation in your namespace (it must be the same implementation as the Mock.DefaultStubBuilder
implementation but has to be in your namespace to build type from your namespace).
Mock myMock = Mock.forType(MyType.class, new MyNamespace.MyStubBuilder());
Use the stub
attribute to access the stub,
MyType myTypeStub = (MyType) myMock.stub;
MyService myServiceInstance = new MyServiceImpl(myTypeStub);
Use the spyOn
method from the mock to spy on a method,
It returns a MethodSpy
instance containing all the tools to drive its behaviour and spy on it
MethodSpy myMethodSpy = myMock.spyOn('myMethod');
By default, a spy return null
when called, whatever the parameters received.
// Act
Object result = myTypeStub.myMethod();
// Assert
Assert.areEqual(null, result);
Have a look at the NoConfiguration recipe
Configure it to return a specific value, whatever the parameter received The stub will always return the configured value
// Arrange
myMethodSpy.returns(new Account(Name='Test'));
// Act
Object result = myTypeStub.myMethod();
// Assert
Assert.areEqual(new Account(Name='Test'), result);
Have a look at the Returns recipe
You can also configure it to return the same object multipe times
// Arrange
myMethodSpy.returns(new Account(Name='Test'), 3);
for(Integer i = 0 ; i < 3 ; ++i) {
// Act
Object result = myTypeStub.myMethod();
// Assert
Assert.areEqual(new Account(Name='Test'), result);
}
Configure it to return a specific value once, whatever the parameter received The stub will return the configured value once
// Arrange
myMethodSpy.returnsOnce(new Account(Name='Test'));
// Act
Object result = myTypeStub.myMethod();
// Assert
Assert.areEqual(new Account(Name='Test'), result);
Have a look at the ReturnsOnce recipe
Configure it to throw a specific exception, whatever the parameter received The stub will always throw the configured exception
// Arrange
myMethodSpy.throwsException(new MyException());
try {
// Act
Object result = myTypeStub.myMethod();
// Assert
Assert.fail('Expected exception was not thrown');
} catch (Exception ex) {
Assert.isInstanceOfType(ex, MyException.class);
}
Have a look at the Throws recipe
You can also configure it to throw the same exception multipe times
// Arrange
myMethodSpy.throwsException(new MyException(), 3);
for(Integer i = 0 ; i < 3 ; ++i) {
try {
// Act
Object result = myTypeStub.myMethod();
// Assert
Assert.fail('Expected exception was not thrown');
} catch (Exception ex) {
Assert.isInstanceOfType(ex, MyException.class);
}
}
Configure it to throw a specific exception once, whatever the parameter received The stub will throw the configured exception once
// Arrange
myMethodSpy.throwsExceptionOnce(new MyException());
try {
// Act
Object result = myTypeStub.myMethod();
// Assert
Assert.fail('Expected exception was not thrown');
} catch (Exception ex) {
Assert.isInstanceOfType(ex, MyException.class);
}
Have a look at the ThrowsOnce recipe
Configure it to return a specific value, when call with specific parameters Configure it to return a specific value once, when call with specific parameters Configure it to return a specific value times N, when call with specific parameters Configure it to throw a specific value, when call with specific parameters Configure it to throw a specific value once, when call with specific parameters Configure it to throw a specific value times N, when call with specific parameters
// Arrange
myMethodSpy
.whenCalledWith(Argument.any(), 10)
.thenReturn(new Account(Name='Test'));
// Arrange
myMethodSpy
.whenCalledWith(Argument.any(), 10)
.thenReturnOnce(new Account(Name='Test Once'));
// Arrange
myMethodSpy
.whenCalledWith(Argument.any(), 10)
.thenReturn(new Account(Name='Test Times'), 3);
// Arrange
myMethodSpy
.whenCalledWith(Argument.any(), -1)
.thenThrow(new MyException);
// Arrange
myMethodSpy
.whenCalledWith(Argument.any(), -1)
.thenThrowOnce(new MyOtherException());
// Arrange
myMethodSpy
.whenCalledWith(Argument.any(), -1)
.thenThrow(new MyException(), 3);
// Act
Object result = myTypeStub.myMethod('nothing', 10);
// Assert
Assert.areEqual(new Account(Name='Test Once'), result);
// Act
Object result = myTypeStub.myMethod('nothing', 10);
// Assert
Assert.areEqual(new Account(Name='Test'), result);
for(Integer i = 0 ; i < 3 ; ++i) {
// Act
Object result = myTypeStub.myMethod('nothing', 10);
// Assert
Assert.areEqual(new Account(Name='Test Times'), result);
}
// Act
try {
myTypeStub.myMethod('value', -1);
// Assert
Assert.fail('Expected exception was not thrown');
} catch (Exception ex) {
Assert.isInstanceOfType(ex, MyOtherException.class);
}
// Act
try {
myTypeStub.myMethod('value', -1);
// Assert
Assert.fail('Expected exception was not thrown');
} catch (Exception ex) {
Assert.isInstanceOfType(ex, MyException.class);
}
for(Integer i = 0 ; i < 3 ; ++i) {
// Act
try {
myTypeStub.myMethod('value', -1);
// Assert
Assert.fail('Expected exception was not thrown');
} catch (Exception ex) {
Assert.isInstanceOfType(ex, MyException.class);
}
}
Have a look at the mocking recipes to have a deeper overview of what you can do with the mocking API.
TL;DR
The order of the spy configuration drive how it will behave.
- If no configuration at all, then return null (default behavior).
- Then, it checks the "matching"
whenCalledWith
once
andtimes
configurations and apply them in setup order. - Then, it checks the "global once" and "global times" (
returnsOnce
orthrowsExceptionOnce
orreturns(object, integer)
orthrows(exception, integer)
) configuration and apply them in setup order. - Then, it checks the "matching"
whenCalledWith
configurations and apply them in setup order. - Then, it checks the "global" (
returns
orthrowsException
) configurations and apply them in setup order.
If there is a configuration and it does not match then it throws a ConfigurationException
.
The error message will contains the arguments and the configuration.
Use it to help you understand the root cause of the issue (configuration/regression/you name it).
The order of the global configuration matters.
If global throw is setup after global returns then throwException
will apply.
myMethodSpy.returns(new Account(Name='Test'));
myMethodSpy.throwsException(new MyException());
Object result = myTypeStub.myMethod(); // throws
If global returns is setup after global throw then returns
will apply
myMethodSpy.throwsException(new MyException());
myMethodSpy.returns(new Account(Name='Test'));
Object result = myTypeStub.myMethod(); // return configured value
For global configuration, the last configured will apply. Same as if you would have configured the spy twice to return (or throw), the last global configuration would be the one kept.
Use the Expect
class to assert on a spy
It exposes the method that
and returns a MethodSpyExpectable
type.
Use the convenient assertion methods the following way:
// hasNotBeenCalled
Expect.that(myMethodSpy).hasNotBeenCalled();
// hasBeenCalled
Expect.that(myMethodSpy).hasBeenCalled();
// hasBeenCalledTimes
Expect.that(myMethodSpy).hasBeenCalledTimes(2);
// hasBeenCalledWith
Expect.that(myMethodSpy).hasBeenCalledWith('stringValue', Argument.any(), true, ...); // up to 5 parameters
Expect.that(myMethodSpy).hasBeenCalledWith(Argument.ofList(new List<Object>{Argument.any(), Argument.any(), ... })); // for more than 5 parameters
// hasBeenLastCalledWith
Expect.that(myMethodSpy).hasBeenLastCalledWith('stringValue', Argument.any(), true, ...); // up to 5 parameters
Expect.that(myMethodSpy).hasBeenLastCalledWith(Argument.ofList(new List<Object>{Argument.any(), Argument.any(), ... })); // for more than 5 parameters
Have a look at the assertions recipes to have a deeper overview of what you can do with the assertion API
Configuring a stub (spy.whenCalledWith(...)
) and asserting (Expect.that(myMethodSpy).hasBeenCalledWith
and Expect.that(myMethodSpy).hasBeenLastCalledWith
) a stub uses Argument.Matchable
interface.
You can either use raw values with notation like spy.whenCallWith('value1', false, ...)
or hasBeenCalledWith(param1, param2, ...)
up to 5 arguments.
It wrapes value with a Argument.equals
when called with any kind of parameter.
When called with a Argument.Matchable
type, it considers it as a parameter, use it directly without wrapping it with a Argument.equals
.
If you need more arguments in your method calls, Argument
offers the ofList
API to create parameters for that, so that you can do spy.whenCallWith(Argument.ofList(new List<Object>{...})))
or hasBeenCalledWith(Argument.ofList(new List<Object>{...}))))
List<Argument.Matchable> emptyParameters = Argument.empty();
List<Argument.Matchable> myMethodParameters = Argument.of(10, 'string'); // Up to five
List<Argument.Matchable> myMethodWithLongParameters = Argument.ofList(new List<Object>{10, 'string', true, 20, false, 'Sure'});
The library provide OOTB (out of the box) Matchables ready for use and fully tested. The library accept your own matchers for specific use cases and reusability.
Argument.any()
matches anything
Argument.any();
Argument.equals()
(the default) matches with native deep equals
Argument.equals(10);
Argument.jsonEquals(new WithoutEqualsType())
matches with json string equals. Convenient to match without equals
type
Argument.jsonEquals(new WithoutEqualsType(10, true, '...'));
Namespaced custom types must add the @JsonAccess
annotation with `serializable='always' to the class when using the unlocked package version.
Argument.ofType()
matches on the parameter type
// To match any Integer
Argument.ofType('Integer');
// To match any Account SObject
Argument.ofType(Account.getSObjectType());
// To match any CustomType class instance
Argument.ofType(CustomType.class);
Use the Argument.Matchable
interface and then use it with Argument
APIs
@isTest
public class MyMatchable implements Argument.Matchable {
public Boolean matches(Object callArgument) {
boolean matches = false;
// custom logic to determine if it matches here
...
return matches;
}
public Boolean equals(Object obj) {
if (obj == null || !(obj instanceof MyMatchable)) {
return false;
}
MyMatchable other = (MyMatchable) obj;
return <this fields custom comparison with other instance>;
}
}
List<Argument.Matchable> args = Argument.of(new MyMatchable(), ...otherArguments);
Implements the public Boolean equals(Object obj)
method on your custom matchable so we can compare list of arguments
Have a look at the overview recipes to have a deeper overview of what you can do with the library
They have their own folder.
It contains usage example for mocking
and asserting
It contains one classe for each use cases the library covers
- No Configuration: spy not configured
- Returns: spy configured to return
- Returns: spy configured to return once
- ReturnsThenThrows: spy configured to throw
- Throws: spy configured to throw
- ThrowsOnce: spy configured to throw once
- ThrowsThenReturns: spy configured to return
- WhenCalledWithCustomMatchable_ThenReturn: spy configured with custom matcher to return
- WhenCalledWithEqualMatching_ThenReturn: spy configured with equals matcher to return
- WhenCalledWithJSONMatching_ThenReturn: spy configured with JSON matcher to return
- WhenCalledWithMatchingThrowsAndReturns: spy configured with matcher to return and to throw
- WhenCalledWithNotMatchingAndReturn: spy configured with matcher and global return, called without matching parameters
- WhenCalledWithTypeMatching_ThenReturn: spy configured with type matcher to return
- WhenCalledWith_ThenReturnOnce: spy configured with a matcher to return once
- WhenCalledWith_ThenThrow: spy configured with a matcher to throw
- WhenCalledWith_ThenThrowOnce: spy configured with a matcher to throw once
- WhenCalledWithoutMatchingConfiguration: spy configured and called without matching parameters
- HasBeenCalled: spy called
- HasBeenCalledTimes: spy called times
- HasBeenCalledWith: spy called with equal matcher
- HasBeenCalledWithCustomMatchable: spy called with custom matcher
- HasBeenCalledWithJSONMatchable: spy called with JSON matcher
- HasBeenCalledWithTypeMatchable: spy called with type matcher
- HasBeenLastCalledWith: spy last called with equal matcher
- HasNotBeenCalled: spy not called
The library repository has 3 parts:
- Test classes in the
force-app/src
folder are what you need to use the lib, no more. Installation button deploy this folder. - Test classes in the
force-app/test
folder are what we need to maintain the library and is not required in production. - Test classes in the
force-app/recipes
folder are what you can use to have a deeper understanding of the library usages.
Considering the concept of test pyramid, the importance of unit test in term of maintainability, how it impacts positively the deployment speed and how the lib can help you doing that, how do I migrate my entire code base to have a well balanced test pyramid ?
Here are the way we suggest to follow to enforce proper unit test
Look for unit test changes at the PR level, ensure the unit test is well decoupled from its dependencies
- Ensure the code base stays clean
- Use the code review stage as an enablement tool for the team
Decouple production code using dependency injection Then rewrite unit tests
- Speed up test execution of this area of the code
- Improve maintainability and Developer Experience
Decouple production code using dependency injection Then write unit tests Then you can either shrink or delete old integrated test
- The apex class under test becomes decoupled from its dependencies
- First step towards SOLID design, you’ll be able to segregate responsibility further
- Antoine Rosenbach Initial contributor
- Lionel Armanet Initial contributor
- Ludovic Meurillon Initial contributor
- Sebastien Colladon
Any contributions you make are appreciated.
See contributing.md for apex-mockery contribution principles.
This project license is BSD 3 - see the LICENSE.md file for details