Background
sinon.stub
/ sandbox.stub
has become the kitchen sink of
configurable behaviour with issues that are often difficult to find and fix without regressions.
I think that the root cause of the difficulties is that it stub
has far too many responsibilities.
Further, stub
also has problematic usage caused by the fact that behaviour is set after it has been created, and can be redefined many times over.
var myStub;
beforeEach(function(){
myStub = sinon.stub().resolves('apple pie :)');
});
// several hundred lines of tests later
myStub = sinon.stub().rejects('no more pie :(');
// several hundred lines of tests later
// what behaviour does myStub currently have? Can you tell without
// reading the entire file?
// can you safely change the behaviour without affecting tests further
// down in the file?
And then there are the more confusing scenarios
var myStub = sinon.stub()
.withArgs(42)
.onThirdCall()
.resolves('apple pie')
.rejects('no more pie')
What does that even do?
Instead of continuing to add more responsibilities to stub
, I propose that instead we introduce new members to sinon
, which are much narrower in scope.
The most important I can think of would be an immutable stand in for a function.
We can then later figure out what we're going to do about properties (as a separate, new, single responsibility member).
sinon.fake
A fake
(the return value of calling sinon.fake
) is a pure and immutable Function
. It does one thing, and one thing only. It has the same behaviour on each and every call. Unlike stub
, its behaviour cannot be redefined. If you need different behaviour, make a new fake
.
Single responsibility
A fake can have one of these responsibilities
- resolve a
Promise
to a value
- reject a
Promise
to an Error
- return a value
- throw an
Error
- yield value(s) to a callback
If you want/need side effects, and you still want the spy interface, then either use the real function, use a stub
or make a custom function
sinon.replace(myObject, myMethod, sandbox.spy(function(args) {
someFunctionWithSideEffects(args);
});
Throws errors generously
Will be generous with throwing errors when user tries to create/use them in unsupported ways.
// will throw TypeError when `config` argument has more than one property
const fake = sinon.fake({
resolves: true,
returns: true
});
Uses the spy API
Except for .withArgs
, as that violates immutability
Usage ideas
// will return a Promise that resolves to 'apple pie'
var fake = sinon.fake({resolves: 'apple pie'})
// will return a Promise that rejects with the provided Error, or
// creates a generic Error using the input as message
var fake = sinon.fake({rejects: new TypeError('no more pie')});
var fake = sinon.fake({rejects: 'no more pie'});
// returns the value passed
var fake = sinon.fake({returns: 'apple pie'});
// throws the provided Error, or creates a generic Error using the
// input as message
var fake = sinon.fake({throws: new RangeError('no more pie')});
var fake = sinon.fake({throws: 'no more pie'});
// replace a method with a fake
var fake = sinon.replace(myObject, 'methodName', sandbox.fake({
returns: 'apple pie'
}))
// .. or use the helper method, which will use `sandbox.replace` and `
// sinon.fake`
var fake = sinon.setFake(global, 'methodName', {
returns: 'apple pie'
});
// create an async fake
var asyncFake = sinon.asyncFake({
returns: 'apple pie'
});
Synonyms
I don't know if fake
is the best noun to use here, but I think we should try to stick to the convention of using nouns, and not stray into adjectives or verbs.
Proposed API changes
Use a default sandbox
This is something I've been considering for awhile, why don't we make a default sandbox? If people need separate sandboxes, they can still create them.
We should create a default sandbox that is used for all methods that are exposed via sinon.*
.
This means that sinon.stub
will become the same as sandbox.stub
, which will remove the limitation of being able to stub properties using sinon.stub
.
sandbox.replace
Create sandbox.replace
and use that for all operations that replace anything anywhere. Expose this as sinon.replace
and use the default sandbox when used this way.
This should probably have some serious input validation, so it'll only replace functions with functions, accessors with accessors, etc.
Feature Request Improvement Needs investigation pinned