Lock Down Sinon Stub

How to ensure your stubs are not called with unexpected arguments.

I like using sinon.js for spying and stubbing on my code during testing. For example, here we have an object with a single method say().

1
2
3
4
5
const o = {
say: () => 'hello'
}
console.log('o.say()', o.say())
// "hello"

I can stub the method o.say to return a known value like this

1
2
3
4
const sinon = require('sinon')
sinon.stub(o, 'say').returns(42)
o.say()
// 42

But during testing we might want to be more precise - if we stub o.say() when called with a certain argument our tests will be stricter. They will ensure that whatever part of the code calls the o.say() also calls it with expected arguments.

1
2
3
4
5
sinon.stub(o, 'say')
.withArgs('foo')
.returns(42)
console.log(o.say('foo'))
// 42

What happens if the stub is called with different arguments? Well, here is the "bad" part - Sinon just returns undefined.

1
2
3
4
5
6
7
sinon.stub(o, 'say')
.withArgs('foo')
.returns(42)
console.log(o.say('foo'))
// 42
console.log(o.say())
// undefined

Hmm, does not seem too bad - and might be even considered a good default behavior. But I often was in a situation in a large unfamiliar codebase where after a small change, my tests would start failing with very weird errors. It was very hard to debug such cases, because often the original stubbed method was returning a promise - and now returning undefined caused all sorts of errors with weird stack traces!

We can lock down the stub by forcing it to throw an error for every unknown argument using stub.throws() method, we can even pass a text message to become a thrown error.

1
2
3
4
5
6
7
sinon.stub(o, 'say')
.throws('nope')
.withArgs('foo').returns(42)
o.say('foo')
// 42
o.say('bar')
// throws Error('nope')

This is great! But the thrown error does NOT tell us what the arguments were that did not match the expected ones. In order to show the arguments that were used to call the stub (and that were unexpected) we can use stub.callsFake() to serialize the arguments. Now the calling code can be found and changed very quickly, even in an unfamiliar codebase.

1
2
3
4
5
6
7
8
function throwError(a) {
throw new Error(`Cannot call this stub with argument ${a}`)
}
sinon.stub(o, 'say')
.callsFake(throwError) // everything else
.withArgs('foo').returns(42)
o.say('bar')
// throws Error('Cannot call this stub with argument bar')

I even wrote a utility not-allowed that does a good job serializing the arguments and throwing the error.

1
2
3
4
5
6
7
const notAllowed = require('not-allowed')
sinon.stub(o, 'say')
.callsFake(notAllowed)
.withArgs('foo').returns(42)
o.say('bar', 'bar', 42)
// Error: Not allowed to call this function with arguments
// foo bar 42

Perfect, no confusion here.