Leighton Wallace
Problems solving, salsa dancing, pun making, software engineer. Also, I'm a black dude.
· 5 min read

How to best use Sinon with Chai

This guide will provide tactical advice on how to use the two modules together while avoiding some common pitfalls.

How to best use Sinon with Chai

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) {
    // When unit testing, you probably don't want this line of code to issue 
    // real http requests.
    // This API's uptime would be a hard dependency for your unit test.
    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() {
  // Throws an error because `request.gettt` does not exist
  sinon.stub(request, 'gettt')
  // Work because request.get is a valid function.
  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:

//Global test helper file
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');
// Allows us to use expect syntax with sinon
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'}
    // Respond with a node-style callback error  
    request.get.yields(requestError); 

    this.apiClient.get('/posts', (err, response) => {
      // Ensure the function was called with expected parameters
      expect(request.get).to.have.been.calledWith('/posts', {apiKey: 'api-key'});
      // Check that the error is the same object that was yielded.
      expect(err).to.equal(requestError);
      return done();
  });
  it('yields INTERNAL_SERVER_ERROR when the response status is 500', function(done) { 
    // Respond with a 500 to simulate a server error
    request.get.yields(null, {status: 500}); 
    
    this.apiClient.get('/posts', (err, response) => {
      // Ensure the function was called with expected parameters
      expect(request.get).to.have.been.calledWith('/posts', {apiKey: 'api-key'});
      // Check that the error is the right string.
      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}); // Respond with a 403
    
    this.apiClient.get('/posts', (err, response) => {
      // Ensure the function was called with expected parameters
      expect(request.get).to.have.been.calledWith('/posts', {apiKey: 'api-key'});
      // Check that the error is the right string.
      expect(err).to.have.property('message', 'AUTH_ERROR')
      // Test for publicly visible side effects
      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!

https://logrocket.com/signup/