@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
PRs are very welcome! Help is much appreciated.
npm install @fluffy-spoon/substitute --save-dev
TypeScript^3.0.0
import{Substitute,Arg}from'@fluffy-spoon/substitute';interfaceCalculator{add(a: number,b: number): number;subtract(a: number,b: number): number;divide(a: number,b: number): number;asyncheavyOperation(): Promise<number>;isEnabled: boolean;}// Create:constcalculator=Substitute.for<Calculator>();// Set a return value:calculator.add(1,2).returns(3);// Check received calls:calculator.received().add(1,Arg.any());calculator.didNotReceive().add(2,2);const calculator = Substitute.for<Calculator>();
See the example below. The same syntax also applies to properties and fields.
// single return typecalculator.add(1,2).returns(4);console.log(calculator.add(1,2));// prints 4console.log(calculator.add(1,2));// prints undefined// multiple return types in sequencecalculator.add(1,2).returns(3,7,9);console.log(calculator.add(1,2));// prints 3console.log(calculator.add(1,2));// prints 7console.log(calculator.add(1,2));// prints 9console.log(calculator.add(1,2));// prints undefinedWhen working with promises you can also use resolves() and rejects() to return a promise.
calculator.heavyOperation(1,2).resolves(4);// same as calculator.heavyOperation(1, 2).returns(Promise.resolve(4));console.log(awaitcalculator.heavyOperation(1,2));// prints 4calculator.heavyOperation(1,2).rejects(newError());// same as calculator.heavyOperation(1, 2).returns(Promise.reject(new Error()));console.log(awaitcalculator.heavyOperation(1,2));// throws Errorcalculator.enabled=true;constfoo=calculator.add(1,2);// verify call to add(1, 2)calculator.received().add(1,2);// verify property set to "true"calculator.received().enabled=true;There are several ways of matching arguments. The examples below also applies to properties and fields - both when setting up calls and verifying them.
import{Arg}from'@fluffy-spoon/substitute';// ignoring first argumentcalculator.add(Arg.any(),2).returns(10);console.log(calculator.add(1337,3));// prints undefined since second argument doesn't matchconsole.log(calculator.add(1337,2));// prints 10 since second argument matches// received call with first arg 1 and second arg less than 0calculator.received().add(1,Arg.is(x=>x<0));import{Arg}from'@fluffy-spoon/substitute';constequalToZero=(x: number)=>x===0;// first argument will match any number// second argument will match a number that is not '0'calculator.divide(Arg.any('number'),Arg.is.not(equalToZero)).returns(10);console.log(calculator.divide(100,10));// prints 10constargIsNotZero=Arg.is.not(equalToZero);calculator.received(1).divide(argIsNotZero,argIsNotZero);// ignoring all argumentscalculator.add(Arg.all()).returns(10);console.log(calculator.add(1,3));// prints 10console.log(calculator.add(5,2));// prints 10The 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));// prints 10console.log(calculator.add(5,2));// prints 10calculator.add(1,3).returns(1337);calculator.add(Arg.all()).returns(10);console.log(calculator.add(1,3));// prints 1337console.log(calculator.add(5,2));// prints 10With 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';classRealCalculatorimplementsCalculator{add(a: number,b: number)=>a+b;subtract(a: number,b: number)=>a-b;divide(a: number,b: number)=>a/b;}constrealCalculator=newRealCalculator();constfakeCalculator=Substitute.for<Calculator>();// let the subtract method always use the real methodfakeCalculator.subtract(Arg.all()).mimicks(realCalculator.subtract);console.log(fakeCalculator.subtract(20,10));// prints 10console.log(fakeCalculator.subtract(1,2));// prints -1// for the add method, we only use the real method when the first arg is less than 10// else, we always return 1337fakeCalculator.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));// prints 105 via real methodconsole.log(fakeCalculator.add(210,7));// prints 1337 via fake method// for the divide method, we only use the real method for explicit argumentsfakeCalculator.divide(10,2).mimicks(realCalculator.divide);fakeCalculator.divide(Arg.all()).returns(1338);console.log(fakeCalculator.divide(10,5));// prints 5console.log(fakeCalculator.divide(9,5));// prints 1338Exceptions can be thrown on properties or methods. You can add different exceptions for different arguments
import{Substitute,Arg}from'@fluffy-spoon/substitute';interfaceCalculator{add(a: number,b: number): number;subtract(a: number,b: number): number;divide(a: number,b: number): number;isEnabled: boolean;}constcalculator=Substitute.for<Calculator>();calculator.divide(Arg.any(),0).throws(newError('Cannot divide by 0'));calculator.divide(1,0);// throws the exception Error: Cannot divide by 0- Easier-to-understand fluent syntax.
- No need to cast to
anyin certain places (for instance, when overriding read-only properties) due to themyProperty.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.
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.
classExample{received(someNumber: number){console.log(someNumber);}}constfake=Substitute.for<Example>();// BAD: this would have called substitute.js' "received" method.// fake.received(2);// GOOD: we now call the "received" method we have defined in the class above.Substitute.disableFor(fake).received(1337);// now we can assert that we received a call to the "received" method.fake.received().received(1337);This project exists thanks to all the people who contribute. [Contribute].
Become a financial contributor and help us sustain our community. [Contribute]
Support this project with your organization. Your logo will show up here with a link to your website. [Contribute]