If you are building software in JavaScript that issues http requests for any reason, you will have code that depends on the responses of these requests. The code that makes those requests has an external dependency that makes unit tests harder to write.

If you are using mocha as a test runner, this is where sinon comes in. It is a full-featured stubbing library for unit testing in JavaScript. It helps you unit test code with external dependencies by allowing you to change the behavior of functions under test.

If you are using jest , it comes with its own stubbing features. The best practices here will have sinon specific examples, but the principles apply to any stubbing engine.

This guide assumes you already know the basics of chai and sinon . This will provide tactical advice on how to use the two modules together while avoiding some common pitfalls.

An api client

Sinon is most useful to avoid relying on external dependencies in unit tests. So, imagine we want to test the get method of this simple api client. It has some special error handling for known response statuses that we want to test:

module .exports = class ApiClient { constructor (httpClient, apiKey) { this .httpClient = httpClient; this .apiKey = apiKey; this .isApiKeyValid = true ; } get(endpoint, callback) { this .httpClient.get(endpoint, { apiKey : this .apiKey}, (err, response) => { if (err) { return callback(err); } if (response.status >= 500 ) { return callback( new Error ( 'INTERNAL_SERVER_ERROR' )) } if (response.status == 403 ) { this .isApiKeyValid = false ; return callback( new Error ( 'AUTH_ERROR' )) } return callback( null , response); }) } }

Stub the real thing

Sinon will throw a very helpful error if the method you attempt to stub doesn’t exist. It is best practice to stub the method you expect to use on the same type of object you use in your code. This will avoid writing unit tests that pass if the code is using non-existent methods:

const request = require ( 'request' ); const sinon = require ( "sinon" ); it( 'issues the request' , function () { sinon.stub(request, 'gettt' ) sinon.stub(request, 'get' ) ... })

Common pitfall: Tests that create completely fabricated objects using sinon.stub() with no arguments can allow tests to pass on code with hard-to-catch typos that lead to bugs.

Sinon expectations

Add the sinon-chai module to the mix in order to use expectation syntax with sinon stubs. Without sinon-chai the expectation can be asserted awkwardly as shown below:

it( 'issues the request' , function ( done ) { sinon.stub(request, 'get' ).yields( null , {}); apiClient = new ApiClient(request, 'api-key' ); apiClient.get( '/endpoint' , (err, response) => { expect(request.get.calledOnce).to.be.true done(err); }) })

On failure, chai will tell us that it “expected false to be true”, which doesn’t provide much context.

Common pitfall: This can make tests harder to maintain for people who didn’t write the original code or test.

With sinon-chai , one can use the same expectation chaining that makes the expect syntax nice to read along with and is better failure reporting:

const request = require ( 'request' ); const sinon = require ( "sinon" ); const chai = require ( "chai" ); const sinon = require ( "sinon" ); const sinonChai = require ( "sinon-chai" ); chai.use(sinonChai); const expect = chai.expect; it( 'issues the request' , function ( done ) { sinon.stub(request, 'get' ).yields( null , {}); apiClient = new ApiClient(request, 'api-key' ); apiClient.get( '/endpoint' , (err, response) => { expect(request.get).to.have.been.calledOnce done(err); }) })

If this fails, sinon-chai will tell us that it “expected request.get to be called once” which is a more accurate explanation of why the test failed.

Always play in the sandbox

Always use a sandbox to store your stubs and spies for easy cleanup. Instead of having to remember to restore each individual stub, the whole sandbox can be restored at once. This will ensure that changes in one test will not bleed over to downstream unit tests:

describe( 'TestModule' , function () { beforeEach( 'setup sandbox' , function () { this .sandbox = sinon.sandbox.create(); this .sandbox.stub(request, 'get' ); }); ... afterEach( 'restore sandbox' function () { this .sandbox.restore(); }); })

This strategy will avoid the common pitfall where stubs and spies remain in effect and alter the behavior of unrelated tests.

If you have any global test setup helpers/infrastructure, consider adding the sandbox restoration to a global afterEach if this.sandbox is set to avoid the test failures that are hard to debug. This can happen if stubs are not cleaned up after a test:

afterEach( 'restore sandbox' , function () { if ( this .sandbox) { this .sandbox.restore(); } }

Use yields for asynchronous interfaces

In many cases, the external dependency will use an asynchronous interface. To test many different results, create the stub once in the beforeEach and use the yields method in your specific test to scope it to that specific case:

const ApiClient = require ( './ApiClient' ); const request = require ( 'request' ); const sinon = require ( 'sinon' ); const chai = require ( 'chai' ); const sinonChai = require ( 'sinon-chai' ); chai.use(sinonChai); const expect = chai.expect; describe( 'ApiClient#get' , function () { beforeEach( 'create ApiClient instance' , function () { this .sandbox = sinon.sandbox.create(); this .sandbox.stub(request, 'get' ) this .apiClient = new ApiClient(request, 'api-key' ); }); afterEach( 'restore stub' , function () { this .sandbox.restore(); } it( 'yields the request error if the request fails' , function ( done ) { let requestError = { some : 'error' } request.get.yields(requestError); this .apiClient.get( '/posts' , (err, response) => { expect(request.get).to.have.been.calledWith( '/posts' , { apiKey : 'api-key' }); expect(err).to.equal(requestError); return done(); }); it( 'yields INTERNAL_SERVER_ERROR when the response status is 500' , function ( done ) { request.get.yields( null , { status : 500 }); this .apiClient.get( '/posts' , (err, response) => { expect(request.get).to.have.been.calledWith( '/posts' , { apiKey : 'api-key' }); expect(err).to.equal( 'INTERNAL_SERVER_ERROR' ); return done(); }); it( 'yields an AUTH_ERROR when the response status is 403' , function ( done ) { request.get.yields( null , { status : 403 }); this .apiClient.get( '/posts' , (err, response) => { expect(request.get).to.have.been.calledWith( '/posts' , { apiKey : 'api-key' }); expect(err).to.have.property( 'message' , 'AUTH_ERROR' ) expect( this .apiClient.isApiKeyValid).to.equal( false ); return done(); }); });

Using yields avoids the common pitfall of creating extra stubs just to act as callbacks for already stubbed methods.

Conclusion

With the tips above, you and your team can better utilize sinon to write unit tests for code with external dependencies while avoiding the common pitfalls listed above!

LogRocket: Full visibility into your web apps LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps. Try it for free.