@fluffy-spoon/substitute
is a TypeScript port of NSubstitute, which aims to provide a much more fluent mocking opportunity for strong-typed languages.
You can read an in-depth comparison of substitute.js
versus other popular TypeScript mocking frameworks here: https://medium.com/@mathiaslykkegaardlorenzen/with-typescript-3-and-substitute-js-you-are-already-missing-out-when-mocking-or-faking-a3b3240c4607
Installing
npm install @fluffy-spoon/substitute --save-dev
Requirements
Usage
import { Substitute, Arg } from '@fluffy-spoon/substitute';
interface Calculator {
add(a: number, b: number): number;
subtract(a: number, b: number): number;
divide(a: number, b: number): number;
isEnabled: boolean;
}
var calculator = Substitute.for<Calculator>();
calculator.add(1, 2).returns(3);
calculator.received().add(1, Arg.any());
calculator.didNotReceive().add(2, 2);
Creating a mock
var calculator = Substitute.for<Calculator>();
Setting return types
See the example below. The same syntax also applies to properties and fields.
calculator.add(1, 2).returns(4);
console.log(calculator.add(1, 2));
console.log(calculator.add(1, 2));
calculator.add(1, 2).returns(3, 7, 9);
console.log(calculator.add(1, 2));
console.log(calculator.add(1, 2));
console.log(calculator.add(1, 2));
console.log(calculator.add(1, 2));
Verifying calls
calculator.enabled = true;
var foo = calculator.add(1, 2);
calculator.received().add(1, 2);
calculator.received().enabled = true;
Argument matchers
There are several ways of matching arguments. The examples below also applies to properties and fields - both when setting up calls and verifying them.
Matching specific arguments
import { Arg } from '@fluffy-spoon/substitute';
calculator.add(Arg.any(), 2).returns(10);
console.log(calculator.add(1337, 3));
console.log(calculator.add(1337, 2));
calculator.received().add(1, Arg.is(x => x < 0));
Ignoring all arguments
calculator.add(Arg.all()).returns(10);
console.log(calculator.add(1, 3));
console.log(calculator.add(5, 2));
Match order
The order of argument matchers matters. The first matcher that matches will always be used. Below are two examples.
calculator.add(Arg.all()).returns(10);
calculator.add(1, 3).returns(1337);
console.log(calculator.add(1, 3));
console.log(calculator.add(5, 2));
calculator.add(1, 3).returns(1337);
calculator.add(Arg.all()).returns(10);
console.log(calculator.add(1, 3));
console.log(calculator.add(5, 2));
Partial mocks
With partial mocks you always start with a true substitute where everything is mocked and then opt-out of substitutions in certain scenarios.
import { Substitute, Arg } from '@fluffy-spoon/substitute';
class RealCalculator implements Calculator {
add(a: number, b: number) => a + b;
subtract(a: number, b: number) => a - b;
divide(a: number, b: number) => a / b;
}
var realCalculator = new RealCalculator();
var fakeCalculator = Substitute.for<Calculator>();
fakeCalculator.subtract(Arg.all()).mimicks(realCalculator.subtract);
console.log(fakeCalculator.subtract(20, 10));
console.log(fakeCalculator.subtract(1, 2));
fakeCalculator.add(Arg.is(x < 10), Arg.any()).mimicks(realCalculator.add);
fakeCalculator.add(Arg.is(x >= 10), Arg.any()).returns(1337);
console.log(fakeCalculator.add(5, 100));
console.log(fakeCalculator.add(210, 7));
fakeCalculator.divide(10, 2).mimicks(realCalculator.divide);
fakeCalculator.divide(Arg.all()).returns(1338);
console.log(fakeCalculator.divide(10, 5));
console.log(fakeCalculator.divide(9, 5));
Benefits over other mocking libraries
- Easier-to-understand fluent syntax.
- No need to cast to
any
in certain places (for instance, when overriding read-only properties) due to the myProperty.returns(...)
syntax. - Doesn't weigh much.
- Produces very clean and descriptive error messages. Try it out - you'll love it.
- Doesn't rely on object instances - you can produce a strong-typed fake from nothing, ensuring that everything is mocked.
Beware
Names that conflict with Substitute.js
Let's say we have a class with a method called received
, didNotReceive
or mimick
keyword - how do we mock it?
Simple! We disable the proxy methods temporarily while invoking the method by using the disableFor
method which disables these special methods.
class Example {
received(someNumber: number) {
console.log(someNumber);
}
}
var fake = Substitute.for<Example>();
Substitute.disableFor(fake).received(1337);
fake.received().received(1337);
Strict mode
If you have strict
set to true
in your tsconfig.json
, you may need to toggle off strict null checks. The framework does not currently support this.
However, it is only needed for your test projects anyway.
{
"compilerOptions": {
"strict": true,
"strictNullChecks": false
}
}