All articles

-

Testing with Sinon

beginnerstestingjs
29 Jan 2020
-

In this article I'm trying to share some basic things that I've learned while using Sinon as my primary stubbing/mocking tool. Hopefully you'll find it useful. If you still have doubts whether unit testing worths your time in general, check out this warm up read. Happy testing! 😁

Our goal

One of the main goals of unit testing is to isolate the unit under test, or, in plainer terms: to avoid (and control) calls to any external dependencies in the flow that you're testing.

External dependencies can be other libraries like aws-sdk or stripe.js, but, it can also be other methods, functions or classes in your own codebase. This is an important aspect, especially for those that are writing tests for the first time, since the word "external" is a bit ambiguous here: it actually means outside your unit under test, not (only) outside your project.

Also, for absolute beginners, the unit under test usually refers to the function you're testing, but we call it unit under test because it could also be a tiny utility class or a couple of functions/methods with intertwined functionality.

Meet Sinon

Sinon.js is a standalone library for testing using fakes, spies, stubs and mocks. Since there's a bit of confusion behind these four terms, and many use them interchangeably (and also because each testing library, including Sinon, has slightly a different view on them) let's start by explaining how they work.

Fakes

These are the easiest ones to understand, but also have limited use (aka, you won't be using them too often). Let's say you're testing a function that takes in a callback for delegation:

function doStuff(data, callback) {
    // do stuff, then delegate some work to the caller
    const result = callback(delegateArgs)
    // do some more stuff using the delegate response
}

When testing such a unit it's useful to easily manipulate the return value or errors thrown by the callback function, while being able to also check its input (arguments) without actually defining multiple implementations.

// in your tests
// create the fake callback
const fakeCallback = sinon.fake()

// call your function with the fake callback
doStuff(data, fakeCallback)

// check the first argument used when calling the callback
fakeCallback.firstArg

Also, a common usecase for fakes is a quick and dirty way to replace some specific functions in third party libraries or system libraries. If your doStuff function would read a file from the filesystem, faking fs.readFile can help you easily test it without actually create a file (or multiple) with the desired contents.

const fake = sinon.fake.yields(null, 'contents')
sinon.replace(fs, 'readFile', fake)
// from here on fs.readFile will always return "contents"

Spies

Name's pretty suggestive. These entities simply record all the arguments, return value and exceptions thrown in the wrapped (spied) function (or object). I find them useful mostly around the publisher/subscriber pattern since in all the other cases you usually need to control the return value or the thrown error as well (i.e. using fakes, stubs or mocks). The main takeway is: spies don't modify the behaviour of the wrapped object/function in any way, that's why you should never use them to wrap over-the-network code or in general with any function call that leaves your process boundaries or accesses out-of-memory resources.

// myclass.js
class MyClass extends EventEmitter {
    doStuff(data) {
        // do stuff then notify others
        this.emit('myevent', data)
    }
}

// myclass.specs.js (aka your tests)
const spy = sinon.spy(MyClass.prototype, 'emit')
const obj = new MyClass()
obj.doStuff(data)
spy.callCount // 1

Stubs

Stubs are probably the ones you're going to use the most in everyday testing. They work exactly like spies, except they also allow you to manipulate return values and thrown errors, all while avoiding a call to the original function.

Mocks

Mocks are a similar to stubs (and thus, with spies): they allow you to record and check all the arguments, errors and return values in the wrapped entity, while avoiding a call to the original implementation. However, they also have built-in expectations. That means you can set the required number of calls, the expected arguments, return value, error and so on, before actually making the function under test call. From the functionality point of view they don't bring anything new over stubs, however, they do change the testing style (expectations first). While I can see their utility, personally, I don't really use them since I find it a little bit clearer to keep the expectations at the end of the unit test (i.e. set up everything, run, expect)

Other actors

Now that we cleared that out, let's move forward and explore two other utility methods that you'll be using fairly often.

The sandbox

Another goal while testing is to have each unit test run in his own environment. That's why we set up all the spied/stubbed/mocked instances before each test and tear them down right after it: we don't want a mock we made in test A to carry on in test B and affect our behaviour or expectations. In order to create a context that facilitates the setting up and tearing down for all of our spies/stubes/mocks, Sinon offers the sandbox class. The way it works is quite straight forward:

beforeEach(function() {
    // create the sandbox
    const sandbox = sinon.createSandbox()

    // spy/stub/mock like you'd use the
    // root sinon object
    sandbox.spy(MyClass.prototype, 'emit')

    // store the sandbox for later use
    this.sandbox = sandbox
})
afterEach(async function() {
    // restore all functions/objects to 
    // their original implementation
    this.sandbox.restore()
})
createStubInstance

This cool little utility method helps you stub all the methods of a class without calling its constructor. This is highly useful in cases when the constructor itself makes calls to other external dependecines or even over the network or filesystem calls (although that's really a bad practice, so please avoid it in your own code)

const mockOcto = sandbox.createStubInstance(Octokit)

// the repos field holds all the functions
// used to manipulate repositories, so we stub it
mockOcto.repos.returns({
    // we further stub the `createUsingTemplate` method
    // we don't care about the return value in this example
    createUsingTemplate: sinon.stub(),
    // same with the delete method
    delete: sinon.stub()
}) 

// inject `mockOcto` in your classes and use it 
// to create and delete repos

// then expect them to be called
expect(mockOcto.repos.createUsingTemplate).to.be.calledOnceWith({
    name: slug,
    template_owner: config.templateOrg,
    template_repo: config.templateRepo,
    owner: config.org
})