Skip to content

Repository files navigation

What is this?

@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.

Installing

npm install @fluffy-spoon/substitute --save-dev

Requirements

  • TypeScript^3.0.0

Usage

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);

Creating a mock

const calculator = Substitute.for<Calculator>();

Setting return types

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 undefined

Working with promises

When 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 4
calculator.heavyOperation(1,2).rejects(newError());// same as calculator.heavyOperation(1, 2).returns(Promise.reject(new Error()));console.log(awaitcalculator.heavyOperation(1,2));// throws Error

Verifying calls

calculator.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;

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';// 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));

Generic and inverse matchers

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);

Note: Arg.is() will automatically infer the type of the argument it's replacing

Ignoring all arguments

// ignoring all argumentscalculator.add(Arg.all()).returns(10);console.log(calculator.add(1,3));// prints 10console.log(calculator.add(5,2));// prints 10

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));// prints 10console.log(calculator.add(5,2));// prints 10
calculator.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 10

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';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 1338

Throwing exceptions

Exceptions 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

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.

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);

Contributors

Code Contributors

This project exists thanks to all the people who contribute. [Contribute].

Financial Contributors

Become a financial contributor and help us sustain our community. [Contribute]

Individuals

Organizations

Support this project with your organization. Your logo will show up here with a link to your website. [Contribute]

About

An NSubstitute port to TypeScript called substitute.js.

Resources

Stars

211 stars

Watchers

5 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages