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! 😁
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.
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.
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"
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 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 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)
Now that we cleared that out, let's move forward and explore two other utility methods that you'll be using fairly often.
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()
})
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
})