Setup
Fakes, spies and stubs are created in the default sandbox which needs to be restored after each test to prevent memory leaks. We use a Mocha hook plugin for that:
// Restores the default sandbox after every test exports.mochaHooks = { afterEach() { sinon.restore(); }, };
Types of test doubles
Type | What it does | Behavior | Pre-programmed behavior | Pre-programmed expectations | When to use |
---|---|---|---|---|---|
Fake | records arguments, return value, the value of this and exception thrown (if any) for all of its calls | immutable | no | Like stubs and spies, but simpler to use and immutable to prevent bugs | |
Spy | like a fake | mutable | no | ||
Stub | like a fake | mutable | yes | no | 1. To control a method’s behavior to force the code down a specific path, e.g. forcing a method to throw an error to test error handling 2. to prevent a specific method from being called directly |
Mock | like a fake | yes | yes | should only be used for the method under test. If you want to control how your unit is being used and like stating expectations upfront (instead of asserting after the fact). In general you should have no more than one mock in a single test |
Fakes
Unlike sinon.spy
and sinon.stub
methods, the sinon.fake
API knows only how to create fakes, and doesn’t concern itself with plugging them into the system under test. To plug the fakes into the system under test, you can use the sinon.replace*
methods. When you want to restore the replaced properties, call the sinon.restore
method.
// Using fakes instead of spies // What it does: "replaces the method "bar" on object foo with the sinon fake" const fake = sinon.replace(foo, "bar", sinon.fake(foo.bar)); // Using fakes instead of stubs const fake = sinon.replace(foo, "bar", sinon.fake.returns("fake value")); // restores all replaced properties set by sinon methods (replace, spy, stub) sinon.restore();
Creating a fake without behavior
Same API as sinon.spy
it("should create fake without behaviour", function () { // create a basic fake, with no behavior const fake = sinon.fake(); assert.isUndefined(fake()); // by default returns undefined assert.equals(fake.callCount, 1); // saves call information });
Creating a fake with custom behavior
Fakes cannot change once created with behavior.
it("should create fake with custom behaviour", function () { // create a fake that returns the text "foo" const fake = sinon.fake.returns("foo"); assert.equals(fake(), "foo"); });
Immutability of a Fake
Because of the immutability of a Fake, this does not work:
const readFake = sinon.fake; // does not work: you try to modify an immutable property sinon.yields(null, ["test.txt"]);
Instead, writing it in one line does the trick:
const readFake = sinon.fake.yields(null, ["test.txt"]);
Creating a fake from an existing function
sinon.fake(func);
Limitations of fakes
onCall()
and withArgs()
does not work with Fakes. A Fake must always have the same behavior. callsFake()
, addBehavior()
and callsThrough()
do not work.
More fake API
// a fake that will throw an error const fake = sinon.fake.throws(new Error("not apple pie")); assert.exception(fake, { name: "Error", message: "not apple pie" }); // returns a resolved or rejected Promise for the passed value sinon.fake.resolves(value); sinon.fake.rejects(value); // asserting calls fake.calledOnce(); fake.getCall(0); fake.calledWith("example");
Testing a NodeJS callback-style method
sinon.fake.yields([value1, ..., valueN])
returns a function that when being called, expects the last argument to be a callback and invokes that callback with the same given values. Normally used to fake a callback-style NodeJS method like readFile
.
Invokes callback synchronously using fake.yields
const assert = referee.assert; const fs = require("fs"); it("should create a fake that 'yields'", function () { const fake = sinon.fake.yields(null, "file content"); const anotherFake = sinon.fake(); sinon.replace(fs, "readFile", fake); fs.readFile("somefile", (err, data) => { // called with fake values given to yields as arguments assert.isNull(err); assert.equals(data, "file content"); // since yields is synchronous, anotherFake is not called yet assert.isFalse(anotherFake.called); sinon.restore(); }); anotherFake(); });
Invokes callback asynchronously using fake.yieldsAsync
const assert = referee.assert; const fs = require("fs"); it("should create a fake that 'yields asynchronously'", function () { const fake = sinon.fake.yieldsAsync(null, "file content"); const anotherFake = sinon.fake(); sinon.replace(fs, "readFile", fake); fs.readFile("somefile", (err, data) => { // called with fake values given to yields as arguments assert.isNull(err); assert.equals(data, "file content"); // since yields is asynchronous, anotherFake is called first assert.isTrue(anotherFake.called); sinon.restore(); }); anotherFake(); });
Spies
Very much like a fake, a spy is a function that records arguments, return value, the value of this
and exception thrown (if any) for all its calls. There are two types of spies: Some are anonymous functions, while others wrap methods that already exist in the system under test.
Creating a spy as an anon function
The spy won’t do anything except record information about its calls.
describe("PubSub", function () { it("should call subscribers on publish", function () { const callback = sinon.spy(); PubSub.subscribe("message", callback); PubSub.publishSync("message"); assertTrue(callback.called); }); });
Creating a spy to wrap all methods of an object
You could use sinon.spy(object)
to spy on all methods of the object, but you should rather try to spy on individual methods instead using sinon.spy(object, "method")
, because this will test your intent more precisely and is less susceptible to unexpected behavior, especially as the object’s code evolves.
The spy will behave exactly like the original method (including when used as a constructor), but you will have access to data about all calls.
In the following test we spy on jQuery.ajax
and check if the first call receives the same url as argument that we pass in originally.
describe("Wrap existing method", function () { const sandbox = sinon.createSandbox(); beforeEach(function () { sandbox.spy(jQuery, "ajax"); }); afterEach(function () { sandbox.restore(); }); it("should inspect jQuery.getJSON's usage of jQuery.ajax", function () { const url = "https://jsonplaceholder.typicode.com/todos/1"; jQuery.getJSON(url); assert(jQuery.ajax.calledOnce); assert.equals(url, jQuery.ajax.getCall(0).args[0].url); assert.equals("json", jQuery.ajax.getCall(0).args[0].dataType); }); });
Observing get and set of an object property
The spies will behave exactly like the original getters and setters, but you will have access to data about all calls.
var object = { get test() { return this.property; }, set test(value) { this.property = value * 2; }, }; var spy = sinon.spy(object, "test", ["get", "set"]); object.test = 42; assert(spy.set.calledOnce); assert.equals(object.test, 84); assert(spy.get.calledOnce);
More spy API
// Creates a spy that only records calls when the received arguments match those passed to withArgs. spy.withArgs(arg1[, arg2, ...]); spy.callCount spy.called spy.notCalled spy.calledOnce spy.calledTwice spy.calledThrice spy.firstCall spy.secondCall spy.thirdCall spy.lastCall spy.calledBefore(anotherSpy) spy.calledAfter(anotherSpy) spy.calledImmediatelyBefore(anotherSpy); spy.calledImmediatelyAfter(anotherSpy); // true if the spy was called at least once with obj as this spy.calledOn(obj); spy.alwaysCalledOn(obj); spy.calledWith(arg1, arg2, ...); spy.calledOnceWith(arg1, arg2, ...); spy.alwaysCalledWith(arg1, arg2, ...); spy.calledWithExactly(arg1, arg2, ...); spy.calledOnceWithExactly(arg1, arg2, ...); spy.alwaysCalledWithExactly(arg1, arg2, ...); spy.calledWithMatch(arg1, arg2, ...); spy.alwaysCalledWithMatch(arg1, arg2, ...); spy.calledWithNew(); spy.neverCalledWith(arg1, arg2, ...); spy.neverCalledWithMatch(arg1, arg2, ...); spy.threw(); spy.threw("TypeError"); spy.threw(obj); spy.alwaysThrew(); spy.alwaysThrew("TypeError"); spy.alwaysThrew(obj); // Returns true if spy returned the provided value at least once. spy.returned(obj); spy.alwaysReturned(obj); var spyCall = spy.getCall(n); var spyCalls = spy.getCalls(); spy.thisValues spy.args spy.exceptions spy.returnValues spy.resetHistory(); // Replaces the spy with the original method. Only available if the spy replaced an existing method spy.restore(); // Returns the passed format string with the following replacements performed: spy.printf("format string", [arg1, arg2, ...]); %n the name of the spy "spy" by default) %c the number of times the spy was called, in words ("once", "twice", etc.) %C a list of string representations of the calls to the spy, with each call prefixed by a newline and four spaces %t a comma-delimited list of this values the spy was called on %n the formatted value of the nth argument passed to printf %* a comma-delimited list of the (non-format string) arguments passed to printf %D a multi-line list of the arguments received by all calls to the spy
Stubs
const stub = sinon.stub().throws(); const stub = sinon.stub(object, "method"); const stub = sinon.stub(obj); // should not be used, rather stub specific methods // Makes the stub return the provided value. stub.returns(obj); // Defines the behavior of the stub on the nth call. Useful for testing sequential interactions. callback.onCall(0).returns(1); // alias stub.onFirstCall(); callback.onCall(1).returns(2); // alias stub.onSecondCall(); callback.returns(3); // Makes the stub call the provided fakeFunction when invoked stub(obj, 'meth').callsFake(fn); // Stubs the method only for the provided arguments. stub.withArgs(arg1[, arg2, ...]); // Resets both behaviour and history of the stub. stub.reset(); // Causes the stub to return the argument at the provided index or TypeError if index is unavailable. stub.returnsArg(index); // return a Promise which resolves/rejects to the provided value stub.resolves(value); stub.rejects(value);
Mocks
Expectations implement both the spies and stubs APIs. Mocks mock the entire module, not single functions.
it("should call all subscribers when exceptions", function () { var myAPI = { method: function () {} }; var spy = sinon.spy(); var mock = sinon.mock(myAPI); mock.expects("method").once().throws(); PubSub.subscribe("message", myAPI.method); PubSub.subscribe("message", spy); PubSub.publishSync("message", undefined); mock.verify(); assert(spy.calledOnce); });
proxyquire
So far, we wrote our spies, stubs, fakes and mocks in the test file, but one really important question is: “How can we achieve that our ‘real’ code is using/invoking the test doubles?”. One way of achieving this would be to pass the test double as a parameter to each ‘real’ function that we want to test:
const myRealCode = require("./myRealModule"); describe('test something', () => { it('should do something', () => { const mySpy = sinon.spy(); myRealCode.doSomething(mySpy); <-- we pass the spy so the code can invoke it expect(mySpy).to.have.been.called; }) });
But doing it that way is not really an option, because we would have to adjust all our methods. The solution is to use proxyquire
module. The idea is: Instead of using require() to import your ‘real’ code into the test, you call proxyquire instead and pass in the test double that shall be used:
const proxyquire = require("proxyquire"); describe('your test suite', function () { it('should test something', function () { const mySpyOne = sinon.spy(); const mySpyTwo = sinon.spy(); const toTestProxy = proxyquire("../../path/to/file/that-we-want/to-test", { // key must be exactly what the import/require statement looks like in your real file // the value is a stub '../whatever/myService.js': mySpyOne, 'jsonwebtoken': mySpyTwo }, ) toTestProxy.someMethod(); }); });
Here is a complete example using Sinon spies:
const Crypto = require("Crypto"); const proxyquire = require("proxyquire"); it('should call Crypto to create a random token', async function() { const testSpy = spy(Crypto, 'randomBytes'); const tokenHandlerProxy = proxyquire('../../src/handlers/tokenHandler', {Crypto}); tokenHandlerProxy.requestToken(); expect(testSpy.called).to.be.true; });