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
- ::classMethod()
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!
Unit Testing with Mocha, Chai, and Sinon
By Brandon Konkle
Unit Testing with Mocha, Chai, and Sinon
This presentation covers why you would ever want to write unit tests for JavaScript in the first place, how to get started with Mocha, using the Chai assertion library, and how to intercept Ajax requests and spy on function calls with Sinon.
- 2,093