Comparison of TypeScript mocking frameworks
This document compares different mocking libraries for TypeScript found on npm.
Key: good points in bold, bad points in italic, neutral in normal font.
Library | safe-mock | OmniMock | Substitute | ts-mockery | ts-mockito |
---|---|---|---|---|---|
Requirements | |||||
TypeScript version | >= 2.1 | >= 3.1 | >=3.0 | unknown | unknown |
Requires Proxy | Yes | Yes | Yes | No | depends3 |
Types of mock | |||||
Backed mocks | No | Yes | No | Yes | No |
Virtual mocks | Yes | Yes | Yes | Yes | Yes |
Function mocks | Yes | Yes | No | Yes | No |
Stubbing features | |||||
Method/function return value | Yes | Yes | Yes | Yes | Yes |
Method/function throw | Yes | Yes | No | No | Yes |
Stub members/getters | No | Yes | No | Yes | Yes |
Call forwarding | No | Yes | No | No | No |
Call fake | No | Yes | Yes | Yes | partial4 |
Deep mocks | No | Yes | No | members only | No |
Verification | |||||
Unexpected calls | partial | Yes | No | Yes | Yes |
Missing calls | Yes | Yes | No | Yes | Yes |
Argument matchers | No | Yes | Yes | partial2 | partial5 |
Verify setters | No | No | Yes | No | No |
Type safety | |||||
Undefined behavior | unknown | No | unknown | unknown | Yes |
Return values | Yes | Yes | Somewhat1 | Yes | Yes |
Function arguments | No | Yes | Yes | Yes | Yes |
Templated functions | Yes | No | No | Yes | Yes |
Developper experience | |||||
Mock is the stub | Yes | No | Yes | Yes | No |
Error messages | - | - | - | - | - |
1Because of the way it deals with the setter-as-stub design, Substitute requires you turn off strict null checking, which some may consider an essential feature of TypeScript.
2Matchers only available for verification, not for expectation setting. This can be worked around by writing some logic in a fake.
3Proxy is only required for virtual mocks.
4The prototype of the original method is completely lost. The fake method is effectively typed as (...any[]) => any
.
5Matchers have type any
This section explains the features presented in the main table.
The Proxy object is a key API for reflectivity in JavaScript. Virtual mocks cannot be achieved without the Proxy API, and the Proxy API can not be polyfilled. You will not be able to use a library relying on Proxy if your tests have to run in an environment which does not support it.
Backed mocks are constructed from an object which exists at runtime. That object becomes the backing instance of the mock and allows features which cannot be achieved with virtual mocks, such as call forwarding and enumerability of the object's property.
Virtual mocks are created from a type only. This lets you mock a class without calling its constructor, or even just an interface.
Virtual mocks can exhibit strange behaviour at runtime because type information is lost at runtime. Besides, virtual mocks can only be implemented using the Proxy API.
The ability to create a mock of a standalone function.
Some libraries may be limited to working with objects and methods of objects, but not functions which are not part of an object.
Undefined behavior happens when the runtime behavior of a type breaks the guarantees offered by its type. Then the outcome of program execution is undefined (aka. "random").
A mocking framework exhibits undefined behavior when it fills in unspecified method calls or member accesses with a default return value which does not match the type of the method or member (most often undefined
or null
). A well designed mocking framework must throw an exception or fail the test whenever behavior for a given action has not been specified, and it is not possible to (type-)safely fallback to a default behavior.
Mocking the value returned by a function or getter.
Mocking a thrown exception upon invocation of the method or getter. Note that if the library supports fakes, then to mock an exception you just create a fake which throws that exception. No big deal.
Mocking the value of a member of an object or class, or the return value of a getter (these are equivalent in JavaScript).
Ability to delegate a call or member access to the backing instance of the mock.
Defining a custom function to call when the mocked method is called or the mocked member is accessed.
Ability to fake the result of a chain of calls or member accesses.
Verifying that no unexpected calls occurred.
Most libraries throw an exception if an unexpected call occurs.
safe-mock returns an object which throws an exception when it is used. This unusual behaviour was most likely chose in order to preserve the merged setter/mock design.
A library which silently returns an undefined value upon unexpected calls violates type safety and fails this criterion.
Ability to verify that the method was called a given number of times.
Argument matchers lets you define different expectations for different invocations of a method based on a set of acceptable parameter values. Similarly, matchers can be used to verify different calls to a method.
Ability to verify that a member of a class or object has been set during the test execution.
Type check the return value provided to a mock.
Checks the type of the arguments passed to the mock call and received by a fake. This includes the type-safety of argument matchers.
Preserve the type safety of functions which take type arguments. For instance:
interface MyVault {
getSecretRecipe<T extends string>(s: T): T extends '5cr37' ? string : number;
}
const mock = createMock<MyVault>();
mock.getSecretReceipe('aaa'); // number
// Some frameworks may think the return value here is `number`, which is wrong.
mock.getSecretReceipe('5cr37'); // string
Whether the object you use to set expectations is the same object you pass to the tested code.
There are conceptually two objects for each mock you create: One is a handler you use to describe the behaviour of the mock, the other is the actual object used by the code you want to test.
Some frameworks chose to merge these two entities into a single object. The result is a lighter syntax and less error prone library because you can't mistake the expectation setter with the mock. On the other hand, using the same symbol for two purposes means it is always ambiguous whether the object is currently being used to set an expectation or by the tested code.
This ambiguity can lead to confusing type or runtime errors depending on how the framework is implemented. It also makes some features harder or impossible to implement.
Frameworks which chose to keep these two functionalities separated are more verbose and more confusing to beginners. However, they generally contain less black magic which makes them more predictable and less prone to edge cases.
A good mocking framework produces descriptive error messages to help the developer. This criteria is one of the most important. Unfortunately, an objective comparison of the quality of error reporting between the different frameworks would require a lot of time and energy. Therefore, this criteria has not been evaluated, but it is shown in the grid to remind the reader of the importance of error reporting. Anyone willing to contribute a review of error reporting in these frameworks would be very valuable.
The following are additional mocking libraries not included in the main comparison. Any feedback would be appreciated.
- ts-mock-imports : Seems to focus on patching imported modules.
- ts-auto-mock : Automatically generates mock objects from a given type, using a TypeScript transformer.
- ts-mocks : Specific to Jasmine.
- strong-mock : States some limitations on getter and call forwarding
- typemoq