https://medium.com/building-ibotta/excelling-with-sinon-js-be35b974b75e
Sinon primarily provides:
Spies (for observation)
to observe calls to service methods
pass through call to original service method
when to use: assume service is robust and reliable, and test only concerns that the code makes the call with correct arguments (and that's enough)
Stubs (for manipulation)
to observe calls and fake responses
not to execute original method
have all of the functionality that spies do
Mocks (stubs + enforced expectations)
Stubs are initialized with arguments/responses, only fake service methods (not called? no problem)
Mocks are initialized with expectations (expected call arguments & fake responses). Not as expected? Fail
Fakes (since v5)
Spies + stubs
Is a function that records everything (arguments, return value, 'this', exceptions) of all its calls
Underlying behavior
Can be created with behavior - wrap an existing function
Can be created without behavior
Immutable - behavior cannot change once created
sinon.fake() only create fakes, does not plug the fake into system under test (like stub and spy does)
use sinon.replace* methods to plug the fake
How a dependent service is initialized and provided to consumer?
Initialize a const in the service module, export that const, import in consumer. Node.js only evaluates the module initialization codes once.
Effect on testing: faking / observing will effect all consumers.
Initialize dependent service in constructor of class (suitable when each consumer require service with different parameters). Effect: allow to be individually manipulated by tests.
Initialize common dependency once (like a db connector), and that common dependency be given to service instances (like an Entity service) at construction time. So a set of services can use shared dependency.
An oversimplified service for demonstration
class FooService {
getFoo(fooId) {
return db.where({ id: fooId });
}
}
Since 5.00 'sinon' is a default sandbox. Call sinon.restore() after each test.
Create sandbox: sinon.createSandbox()
From the created sandbox creates stubs, fakes, spies:
sandbox.stub...
Then after each test sandbox.restore()
Replaces
sandbox.replace(myObject, 'myMethod', function () {......});
sandbox.replaceGetter(myObject, 'myMethod', function () {return ......});
sandbox.replaceSetter(object, 'myProperty', function (value) { this.prop = ......});
Spy, stub, fake...
sandbox.spy() = sinon.spy
sandbox.createStubInstance() like sandbox.stub()/sinon.stub() but also adds the created to internal collection for easy restoring
sandbox.mock() = sinon.mock
Fake things
sandbox.useFakeTimers(); binds 'clock' object to sandbox so it too can be restored
sandbox.useFakeXMLHttpRequest(); binds resulting object to sandbox so it too be restored
sandbox.useFakeServer(); fakes XHR & binds a server object
sandbox.usingPromise(promiseLibrary);
Restore / reset
sandbox.restore();
sandbox.reset(); // reset initial state of all fakes
sandbox.resetBehavior();
sandbox.resetHistory();
Verify
sandbox.verify(); // verify all mocks
sandbox.verifyAndRestore();
it('invokes the database', async function() {
const subject = new FooService();
sinon.spy(subject.db, 'where');
await subject.getFoo(1234);
const dbArgs = db.where.getCall(0).args[0];
expect(dbArgs.id).to.eql(1234);
});
Basic usage
it('returns records from the database', async function() {
const recordInDb = { id: 1234, name: 'bar' };
const subject = new FooService();
sinon.stub(subject.db, 'where')
.returns(Promise.resolve([recordInDb]));
const result = await subject.getFoo(1234);
expect(result).to.deep.eql(recordInDb);
});
Requiring arguments
sinon.stub(subject.db, 'where')
.withArgs({ id: 1234 })
.returns(Promise.resolve([recordInDb]));
Multiple withArgs/return pairs
const stub = sinon.stub(subject.db, 'where');
stub.withArgs({ id: 1234 }).returns(Promise.resolve([record1]));
stub.withArgs({ id: 5678 }).returns(Promise.resolve([record2]));
Return on call count
sinon.stub(subject.db, 'where').onCall(3).returns(...);
Matching (test should only concerns relevant input and ignores unrelated ones, so as to not be affected by future changes)
sinon.stub(friendsService, 'getFriendsForUser')
.withArgs(sinon.match({ id: expectedId }))
.returns(Promise.resolve(friends));
Fake asynchronous function returns
.... .returns(Promise.resolve(value)) // returns a Promise
.... .resolves(value) // the shorthand style
it('persists the value to the db', function() {
sinon.mock(db)
.expects('update')
.withArgs({ id: 1234, value: 1 })
.resolves(true);
const subject = new CounterService();
const result = subject.increment(1234, 5);
expect(result).to.eql(6);
});
When faked method not invoked as expected, test will fail.
var fake = sinon.fake(); // fake without behavior
var fake = sinon.fake.returns('apple pie'); // create fake with return value
var fake = sinon.fake.throws(new Error('not apple pie')); // create fake that...
var fake = sinon.fake.resolves(value) // create async fake that returns a Promise that resolves...
var fake = sinon.fake.rejects(value); // same as above but rejects...
// yields... I find the document confusing. Later...
Make default spies and stub in "beforeEach" blocks
Use mocks in specific tests - because they contain expectations
Use sandbox
sinon-chai for assertion syntax