Unit Testing
with Mocha, Chai, and Sinon

Brandon Konkle

Craftsy

@bkonkle

Why unit test?

Unit testing helps you write better code!

 

  • Think about your code differently
  • Plan ahead for how your code will be used
  • Clearly define your code's responsibilities
  • Identify things that could break and find
    solutions ahead of time
  • Demonstrate how your code should and
    should not be used
  • Make big changes confidently and quickly

Integration Tests

Tests that describe how a user interacts with your application and check the results based on what the user should see. All of the components of your app are tested together, fully integrated.

Unit Tests

Tests that evaluate the output of a small piece of functionality given a set of inputs. The code is as isolated from other functionality as possible, with external calls mocked out so that they don't affect the test.

Mocha

A test runner. It provides:

  • Structure: describe, it
  • Hooks: before, after
  • Reporting pass/fail
/* global describe, it, beforeEach */
'use strict';

var SonicScrewdriver = require('./src/SonicScrewdriver');

describe('SonicScrewdriver', function() {

  describe('(constructor)', function() {

    it('Establishes a mental connection with the user', function() {
      // ** Test code goes here **
    });

    // ** More tests go here **

  });

  describe('#handleCommand()', function() {

    beforeEach(function() {
      // ** Setup that is run before each test in this group **
    });

    it('Responds to mental commands from the user', function() {
      // ** Mose test code goes here **
    });

    // ** More tests go here **

  });

});

Common Structure

  • Class (or module name)
    • ::classMethod()
      • Test
      • Test
    • (constructor)
      • Test
      • Test
    • #instanceMethod()
      • Test
      • Test

Always place your before

and after functions in describe blocks.

Results

Chai

An assertion library. It provides:

  • Styles: assert, expect, should
  • Assertions:
    • .isTrue()
    • .equal()
    • .isArray()
    • .instanceOf()
    • .throws()
// Assert style

assert.equal(whoAmI, 'Groot', 'I am Groot');
 
assert.isTrue(teaServed, 'the tea has been served');
 
assert.instanceOf(chai, Tea, 'chai is an instance of tea');


// Expect style

expect(whoAmI).to.equal('Groot');
 
expect(teaServed).to.be.true;
 
expect(chai)to.be.an.instanceOf(Tea);


// Should style

whoAmI.should.equal('Groot');

teaServed.should.be.true;

chai.should.be.an.instanceOf(Tea);

Assertions/Expectations

  • is true, false, null, undefined
  • equality (shallow and deep)
  • length
  • above, below, within a range
  • type or class identity
  • has or inherited a property
  • matches a regex
  • throws an error

Sinon

Mocking library. Provides:

  • spy()
  • stub()
  • mock()
  • fake XHR

Use it to

  • Isolate distinct functionality
  • Remove dependencies on external code
  • Provide predictable responses for
    internal or external interaction

Spies

  • Track function calls
  • Record arguments
  • Record what is returned
  • Record errors thrown
  • Record the this value

 

  • Can wrap an existing function
  • Can fully intercept a function

Stubs

  • Do everything a spy does
  • Allow for pre-programmed responses
  • Even different responses for certain args
  • Can throw errors

Mocks

  • Do everything spies and stubs do
  • Provide build in expectations that can fail your test

 

Mocks are not generally needed.

It's often clearer to use Chai assertions with the spy tracking.

'use strict';

var chai = require('chai');
var sinon = require('sinon');
var SonicScrewdriver = require('./src/SonicScrewdriver');

var expect = chai.expect;

describe('SonicScrewdriver', function() {

  describe('(constructor)', function() {

    var testConnection = sinon.spy();
    var origGetConnection;

    before(function() {
      // Save the original getConnection functionality
      origGetConnection = SonicScrewdriver.getConnection;
    });

    beforeEach(function() {
      // Set up a fresh stub for the mental connection code
      SonicScrewdriver.getConnection = sinon.stub().returns(testConnection);
    });

    after(function() {
      // Restore the original getConnection functionality
      SonicScrewdriver.getConnection = origGetConnection;
    });

    it('Establishes a mental connection with the user', function() {
      var testScrewdriver = new SonicScrewdriver();

      expect(SonicScrewdriver.getConnection).to.have.been.called;
      expect(testScrewdriver._connection).to.equal(testConnection);
    });

  });

Fake XHR

  • Intended for testing within a browser (or a PhantomJS environment)
  • Replaces the built-in XMLHttpRequest, jQuery isn't touched
  • Requests aren't actually sent
  • You can log requests to a shared object
  • You can send responses to each request, triggering callbacks
var xhr, requests;
 
beforeEach(function() {
    // Use Sinon to intercept Ajax requests
    xhr = sinon.useFakeXMLHttpRequest();
    requests = [];
    xhr.onCreate = function (xhr) {
        requests.push(xhr);
    };
});
 
after(function() {
    xhr.restore();
});

Fake XHR Setup

Checking Requests

// One Ajax request should be sent
expect(requests).to.have.length(1);

var request = requests[0];

// It should be hitting the screwdriver endpoint
expect(request.url).to.equal('/api/v1/screwdriver');

// It should be a POST request
expect(request.method).to.equal('POST');

// The body should contain a screwdriver id
expect(request.requestBody).to.contain('id=' + testScrewdriver.id);

Setting Responses

var request = requests[0];

var status = 200;
var headers = {'Content-Type': 'application/json'}
var body = JSON.stringify({'success': true});
request.respond(status, headers, body);

Learn more!

Made with Slides.com