apex-mockery
apex-mockery copied to clipboard
Lightweight mocking library in Apex
Lightweight mocking library in Apex
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
- Principles
- Why you should use the library
- Installation
- Namespaced Org /!\
- Usage
- Mock
- How to stub namespaced type?
- Stub
- Spy
- How to Configure a spy
- Default behaviour
- Global returns
- Global throws
- Parameterized configuration
- Configuration order matters !
- How to Configure a spy
- Assert on a spy
- Arguments
- Argument matcher
- Any
- Equal
- jsonEqual
- ofType
- BYOM (Build your own matcher)
- Recipes
- Mocking
- Asserting
- Mock
- Library architecture
- How to migrate my codebase?
- When implementing new feature
- When touching existing/legacy code
- When refactoring
- Authors
- Contributing
- License
Principles
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
Why you should use the library
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).
Installation
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
Namespaced Org /!\
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).
Usage
Mock
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);
How to stub namespaced type?
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());
Stub
Use the stub attribut to access the stub,
MyType myTypeStub = (MyType) myMock.stub;
MyService myServiceInstance = new MyServiceImpl(myTypeStub);
Spy
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');
How to Configure a spy
Default behaviour
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
Global returns
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
Global throws
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
Parameterized configuration
Configure it to return a specific value, when call with specific parameters Configure it to throw a specific value, when call with specific parameters
// Arrange
myMethodSpy
.whenCalledWith(Argument.any(), 10)
.thenReturn(new Account(Name='Test'));
// Arrange
myMethodSpy
.whenCalledWith(Argument.any(), -1)
.thenThrow(new MyException);
// Act
Object result = myTypeStub.myMethod('nothing', 10);
// Assert
Assert.areEqual(new Account(Name='Test'), result);
// Act
try {
Object result = 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.
Configuration order matters !
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
whenCalledWithconfigurations. - Then, it checks the global
returnsconfigurations. - Then, it checks the global
throwsExceptionconfigurations.
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.
Assert on a spy
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
Arguments
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'});
Argument matcher
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.
Any
Argument.any() matches anything
Argument.any();
Equal
Argument.equals() (the default) matches with native deep equals
Argument.equals(10);
jsonEqual
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.
ofType
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);
BYOM (Build your own matcher)
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;
}
}
List<Argument.Matchable> args = Argument.of(new MyMatchable(), ...otherArguments);
Have a look at the overview recipes to have a deeper overview of what you can do with the library
Recipes
They have their own folder.
It contains usage example for mocking and asserting
It contains one classe for each use cases the library covers
Mocking
- No Configuration: spy not configured
- Returns: spy configured to return
- ReturnsThenThrows: spy configured to throw
- Throws: spy configured to throw
- 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_ThenThrow: spy configured with JSON matcher to throw
- WhenCalledWithoutMatchingConfiguration: spy configured and called without matching parameters
Asserting
- 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
Library architecture
The library repository has 3 parts:
- Test classes in the
force-app/srcfolder are what you need to use the lib, no more. Installation button deploy this folder. - Test classes in the
force-app/testfolder are what we need to maintain the library and is not required in production. - Test classes in the
force-app/recipesfolder are what you can use to have a deeper understanding of the library usages.

How to migrate my codebase?
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
When implementing new feature
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
When touching existing/legacy code
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
When refactoring
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
Authors
- Antoine Rosenbach Initial contributor
- Lionel Armanet Initial contributor
- Ludovic Meurillon Initial contributor
- Sebastien Colladon
Contributing
Any contributions you make are appreciated.
See contributing.md for apex-mockery contribution principles.
License
This project license is BSD 3 - see the LICENSE.md file for details