JavaScript Unit Testing
DevTalk 16-03-17
Tests?
What is software testing?
- Testing is the process of establishing confidence that a program or system does what it is supposed to (Hetzel, 1973).
- Testing is the process of executing a program or system with the intent of finding errors (Myers, 1979).
- Testing is demonstrating that a system is fit for purpose (Evans, et al, 1996)
- Blah, blah, blah...
Type of tests
-
Functionality Testing
-
Usability testing
-
Interface testing
-
Compatibility testing
-
Performance testing
-
Security testing
-
Unit testing
-
Integration testing
-
...
Unit tests
ensure that individual components of the app work as expected. Assertions test the component API.
Test hierarchy
Source: The Benefits of Unit testing
The goal of unit testing is:
- to isolate each part of the program,
- show that the individual parts are correct.
A unit test should NOT:
- access the network,
- hit a database,
- use the file system,
- call other non-trivial components,
- .... no side-effects in unit tests.
Source: Unit and integration testing
Mocha
What is this?
Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases.
Example of installation and run
-
npm install mocha
- example file to test:
- in package.json:
- dsf
- sd
-
npm test
var assert = require('assert');
describe('Array', function() {
describe('#indexOf()', function() {
it('should return -1 when the value is not present', function() {
[1,2,3].indexOf(4).should.equal(-1)
});
});
});
"scripts": {
"test": "mocha"
}
Assertions
-
should.js - BDD style shown throughout these docs
-
expect.js - expect() style assertions
-
chai - expect(), assert() and should-style assertions
-
better-assert - C-style self-documenting assert()
-
unexpected - “the extensible BDD assertion toolkit”
Interfaces
describe('#indexOf()', function() {
context('when not present', function() {
it('should not throw an error', function() {
specify('if is equal to 4', function() {
});
});
});
});
-
describe() - header for a section of test
-
context() - is just an alias for
describe()
, and behaves the same way; it just provides a way to keep tests easier to read and organized. -
it() - for a specific testing concern
-
specify() - similar as context to describe but to it
Asynchronous code
-
callback:
describe('User', function() {
describe('#save()', function() {
it('should save without error', function(done) {
var user = new User('Luna');
user.save(done);
});
});
});
- promise:
beforeEach(function() {
return db.clear()
.then(function() {
return db.save([tobi, loki, jane]);
});
});
describe('#find()', function() {
it('respond with matching records', function() {
return db.find({ type: 'User' }).should.eventually.have.length(3);
});
});
Hooks
describe('hooks', function() {
before(function() {
// runs before all tests in this block
});
after(function() {
// runs after all tests in this block
});
beforeEach(function() {
// runs before each test in this block
});
afterEach(function() {
// runs after each test in this block
});
// test cases
});
Exclusive tests
describe('Array', function() {
describe('#indexOf()', function() {
it.only('should return -1 unless present', function() {
// this test will be run
});
it('should return -1 if called with a non-Array context', function() {
// this test will not be run
});
});
});
describe('Array', function() {
describe.only('#indexOf()', function() {
it('should return -1 unless present', function() {
// this test will be run
});
it('should return the index when present', function() {
// this test will also be run
});
});
Inclusive tests
describe('Array', function() {
describe.skip('#indexOf()', function() {
// ...
});
xdescribe('#indexOf()', function() {
xit('x simple', () => {
// ...
});
it('simple', () => {
// ...
});
});
});
it('should only test in the correct environment', function() {
if (/* check test environment */) {
// make assertions
} else {
this.skip();
}
});
Mocha with bundlers
- Webpack: https://www.npmjs.com/package/mocha-webpack
- Grunt: https://github.com/kmiyashiro/grunt-mocha
- Gulp: https://github.com/sindresorhus/gulp-mocha
Chai
What is this?
Chai is a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework.
Chai interfaces
- Should extends each object with a should property to start your chain.
chai.should();
foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.lengthOf(3);
tea.should.have.property('flavors').with.lengthOf(3);
Chains:
to, be, been, is, that, which, and, has, have, with, at, of, same
Chai interfaces
- Expect is function. After function call you can start your chain.
var expect = chai.expect;
expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.lengthOf(3);
expect(beverages).to.have.property('tea').with.lengthOf(3);
Chains:
to, be, been, is, that, which, and, has, have, with, at, of, same
Chai interfaces
- Assert provides the classic assert-dot notation, similar to that packaged with node.js.
var assert = require('chai').assert
, foo = 'bar'
, beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };
assert.typeOf(foo, 'string'); // without optional message
assert.typeOf(foo, 'string', 'foo is a string'); // with optional message
assert.equal(foo, 'bar', 'foo equal `bar`');
assert.lengthOf(foo, 3, 'foo`s value has a length of 3');
assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');
Plugins
-
Chai Match
npm i --save-dev chai-match
var chai = require('chai');
chai.use(require('chai-match'));
expect('some thing to test').to.match(/some (\w+) to test/).and.capture(0).equals('thing');
-
Chai Deep Match
npm install --save chai-deep-match
var chai = require('chai');
var chaiDeepMatch = require('chai-deep-match');
chai.use( chaiDeepMatch );
chai.expect( { a: 'foo', b: 'bar', c: 'baz' } ).to.deep.match( { a: 'foo', c: 'baz' } );
-
Chai Immutable
-
and others
Sinon.JS
Sinon.js is a vital tool when writing JavaScript unit tests. Works with any unit testing framework.
Sinon allows you to replace the difficult parts of your tests with something that makes testing simple.
Mainly you can use:
- spies
- stubs
- mocks
Spies
Which offer information about function calls, without affecting their behavior.
A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls.
Spies
function setupNewUser(info, callback) {
var user = {
name: info.name,
nameLowercase: info.name.toLowerCase()
};
try {
Database.save(user, callback); //send ajax request
}
catch(err) {
callback(err);
}
}
We can check how many times a function was called
it('should call save once', function() {
var save = sinon.spy(Database, 'save');
setupNewUser({ name: 'test' }, function() { });
save.restore();
sinon.assert.calledOnce(save);
});
it('should pass object with correct values to save', function() {
var save = sinon.spy(Database, 'save');
var info = { name: 'test' };
var expectedUser = {
name: info.name,
nameLowercase: info.name.toLowerCase()
};
setupNewUser(info, function() { });
save.restore();
sinon.assert.calledWith(save, expectedUser);
});
We can check what arguments were passed to a function
Stubs
Which are like spies, but completely replace the function. This makes it possible to make a stubbed function do whatever you like — throw an exception, return a specific value, etc.
Test stubs are functions (spies) with pre-programmed behavior.
They support the full test spy API in addition to methods which can be used to alter the stub’s behavior.
it('should pass object with correct values to save', function() {
var save = sinon.stub(Database, 'save');
var info = { name: 'test' };
var expectedUser = {
name: info.name,
nameLowercase: info.name.toLowerCase()
};
setupNewUser(info, function() { });
save.restore();
sinon.assert.calledWith(save, expectedUser);
});
Stubs can be used to replace problematic code
Stubs can also be used to trigger different code paths
it('should pass the error into the callback if save fails', function() {
var expectedError = new Error('oops');
var save = sinon.stub(Database, 'save');
save.throws(expectedError);
var callback = sinon.spy();
setupNewUser({ name: 'foo' }, callback);
save.restore();
sinon.assert.calledWith(callback, expectedError);
});
Thirdly, stubs can be used to simplify testing asynchronous code.
it('should pass the database result into the callback', function() {
var expectedResult = { success: true };
var save = sinon.stub(Database, 'save');
save.yields(null, expectedResult);
var callback = sinon.spy();
setupNewUser({ name: 'foo' }, callback);
save.restore();
sinon.assert.calledWith(callback, null, expectedResult);
});
Mocks
Which make replacing whole objects easier by combining both spies and stubs
Mocks (and mock expectations) are fake methods (like spies) with pre-programmed behavior (like stubs) as well as pre-programmed expectations.
A mock will fail your test if it is not used as expected.
it('should pass object with correct values to save only once', function() {
var info = { name: 'test' };
var expectedUser = {
name: info.name,
nameLowercase: info.name.toLowerCase()
};
var database = sinon.mock(Database);
database.expects('save').once().withArgs(expectedUser);
setupNewUser(info, function() { });
database.verify();
database.restore();
});
Mocks should be used primarily when you would use a stub, but need to verify multiple more specific behaviors on it.
Enzyme
Shallow Rendering
import { shallow } from 'enzyme';
describe('<MyComponent />', () => {
it('should render three <Foo /> components', () => {
const wrapper = shallow(<MyComponent />);
expect(wrapper.find(Foo)).to.have.length(3);
});
it('simulates click events', () => {
const onButtonClick = sinon.spy();
const wrapper = shallow(
<Foo onButtonClick={onButtonClick} />
);
wrapper.find('button').simulate('click');
expect(onButtonClick.calledOnce).to.equal(true);
});
});
Shallow rendering is useful to constrain yourself to testing a component as a unit, and to ensure that your tests aren't indirectly asserting on behavior of child components.
Shallow Rendering (2)
Simple shallow calls:
- constructor,
- render
Shallow + setProps calls:
- componentWillReceiveProps,
- shouldComponentUpdate,
- componentWillUpdate,
- render
Shallow + unmount calls:
- componentWillUnmount
Full Rendering
import { mount } from 'enzyme';
import sinon from 'sinon';
import Foo from './Foo';
describe('<Foo />', () => {
it('calls componentDidMount', () => {
sinon.spy(Foo.prototype, 'componentDidMount');
const wrapper = mount(<Foo />);
expect(Foo.prototype.componentDidMount.calledOnce).to.equal(true);
});
it('allows us to set props', () => {
const wrapper = mount(<Foo bar="baz" />);
expect(wrapper.props().bar).to.equal("baz");
wrapper.setProps({ bar: "foo" });
expect(wrapper.props().bar).to.equal("foo");
});
});
Full DOM rendering is ideal for use cases where you have components that may interact with DOM APIs, or may require the full lifecycle in order to fully test the component (i.e., componentDidMount etc.)
Full Rendering (2)
Simple mount calls
- constructor
- render
- componentDidMount
Mount + setProps calls
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
Mount + unmount calls
- componentWillUnmount
Static Rendering
import { render } from 'enzyme';
describe('<Foo />', () => {
it('renders three `.foo-bar`s', () => {
const wrapper = render(<Foo />);
expect(wrapper.find('.foo-bar')).to.have.length(3);
});
it('rendered the title', () => {
const wrapper = render(<Foo title="unique" />);
expect(wrapper.text()).to.contain("unique");
});
});
Enzyme's render function is used to render react components to static HTML and analyze the resulting HTML structure.
only calls render but renders all children
Summary
- Always begin with shallow
- If componentDidMount or componentDidUpdate should be tested, use mount
- If you want to test component lifecycle and children behavior, use mount
- If you want to test children rendering with less overhead than mount and you are not interested in lifecycle methods, use render
React
Components
export default class Simple extends PureComponent {
static propTypes = {
data: PropTypes.object,
simpleFlag: PropTypes.bool,
getSimpleData: PropTypes.func.isRequired
};
componentWillMount() {
this.props.getSimpleData(this.props.simpleFlag);
}
render() {
return (
<div className="simple">
{this.props.simpleFlag ?
<h1 className="simple__title">
<FormattedMessage {...messages.title} />
</h1>
: null}
<SimpleList data={this.props.data} />
</div>
);
}
}
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import { spy } from 'sinon';
import { FormattedMessage } from 'react-intl';
import Simple from '../simple.component';
import SimpleList from '../simpleList/simpleList.component';
describe('Simple: Component', () => {
const defaultProps = {
getSimpleData: () => {},
data: {a: 'a', b: 'b'},
};
const component = (props = {}) => (
<Simple {...defaultProps} {...props} />
);
it('should render Simple root', () => {
const wrapper = shallow(component());
expect(wrapper.find('.simple'))
.to.have.length(1);
});
it('should dispatch getSimpleData action on mount', () => {
const getSimpleData = spy();
shallow(component({ getSimpleData }));
expect(getSimpleData.calledOnce)
.to.be.equal(true);
});
it('should dispatch getSimpleData action with proper arguments', () => {
const getSimpleData = spy();
shallow(component({ getSimpleData }));
expect(getSimpleData.getCall(0).args[0])
.to.be.equal(defaultProps.simpleFlag);
});
it('should pass data into <SimpleList />', () => {
const wrapper = shallow(component());
expect(wrapper.find(SimpleList).prop('data'))
.to.be.equal(defaultProps.data);
});
describe('Message Rendering: ', () => {
it('should render message', () => {
const wrapper = shallow(component({ simpleFlag: true }));
expect(wrapper.find(FormattedMessage))
.to.have.length(1);
});
it('should not render message', () => {
const wrapper = shallow(component({ simpleFlag: false }));
expect(wrapper.find(FormattedMessage))
.to.have.length(0);
});
});
});
Redux
introduction
Action
Reducer
Store
View
Actions
import { SUBMIT, SUBMIT_SUCCESS, SUBMIT_FAIL } from './simple.constants';
export function submit( email, locale ) {
return {
type: SUBMIT,
payload: { email, locale },
};
}
export function errorSubmitData() {
return {
type: SUBMIT_FAIL,
};
}
export function successSubmitData() {
return {
type: SUBMIT_SUCCESS,
};
}
import { expect } from 'chai';
import { SUBMIT, SUBMIT_SUCCESS, SUBMIT_FAIL } from '../simple.constants';
import { submit, errorSubmitData, successSubmitData } from '../simple.actions';
describe('Simple: actions', () => {
const email = 'simple@test.com';
const locale = 'en';
describe('actions on SUBMIT', () => {
it('should return SUBMIT type', () => {
expect(submit().type).to.equal(SUBMIT);
});
it('should return payload', () => {
expect(submit(email, locale).payload).to.deep.equal({email, locale});
});
});
describe('actions on SUBMIT_FAIL', () => {
it('should return SUBMIT_FAIL type', () => {
expect(errorSubmitData().type).to.equal(SUBMIT_FAIL);
});
});
describe('actions on SUBMIT_SUCCESS', () => {
it('should return SUBMIT_SUCCESS type', () => {
expect(successSubmitData().type).to.equal(SUBMIT_SUCCESS);
});
});
});
Reducers
import { Record } from 'immutable';
import { SUBMIT, SUBMIT_FAIL, SUBMIT_SUCCESS } from './simple.constants';
const StateRecord = new Record({
data: null,
isLoading: false,
isSent: null,
});
const initialState = new StateRecord({});
function simpleReducer(state = initialState, action) {
switch (action.type) {
case SUBMIT:
return state
.set('isLoading', true);
case SUBMIT_FAIL:
return state
.set('isLoading', false)
.set('isSent', false);
case SUBMIT_SUCCESS:
return state
.set('isLoading', false)
.set('isSent', true);
default:
return state;
}
}
export default simpleReducer;
import { expect } from 'chai';
import { fromJS } from 'immutable';
import reducer from '../simple.reducer';
import { SUBMIT, SUBMIT_FAIL, SUBMIT_SUCCESS } from '../simple.constants';
describe('Simple: reducer', () => {
const state = fromJS({
data: null,
isLoading: false,
isSent: null,
});
it('should return state on unknown action', () => {
expect(reducer(state, { type: 'unknown-action' })).to.be.equal(state);
});
describe('states on SUBMIT', () => {
it('should return state on SUBMIT', () => {
const isLoading = true;
const expectedState = state.set('isLoading', isLoading);
expect(reducer(state, {
isLoading, type: SUBMIT
}).equals(expectedState)).to.be.equal(true);
});
});
describe('states on SUBMIT_FAIL', () => {
it('should set isLoading false on SUBMIT_FAIL', () => {
const isLoading = false;
const expectedState = state.set('isLoading', isLoading);
expect(reducer(state, {
isLoading, type: SUBMIT_FAIL
}).equals(expectedState)).to.be.equal(false);
});
it('should set isSent true on SUBMIT_FAIL', () => {
const isSent = false;
const expectedState = state.set('isSent', isSent);
expect(reducer(state, {
isSent, type: SUBMIT_FAIL
}).equals(expectedState)).to.be.equal(true);
});
});
describe('states on SUBMIT_SUCCESS', () => {
it('should set isLoading false on SUBMIT_SUCCESS', () => {
const isLoading = false;
const expectedState = state.set('isLoading', isLoading);
expect(reducer(state, {
isLoading, type: SUBMIT_SUCCESS
}).equals(expectedState)).to.be.equal(false);
});
it('should set isSent true on SUBMIT_SUCCESS', () => {
const isSent = true;
const expectedState = state.set('isSent', isSent);
expect(reducer(state, {
isSent, type: SUBMIT_SUCCESS
}).equals(expectedState)).to.be.equal(true);
});
});
});
Reselects
import { createSelector } from 'reselect';
const selectSimpleDomain = state => state.simpleDomain;
export const selectDomainName = createSelector(
selectSimpleDomain, state => state.get('name')
);
export const selectData = createSelector(
selectSimpleDomain, state => state.get('data')
);
export const filterData = createSelector(
selectData, data => data.filter(({type}) => type === 'free')
);
import { expect } from 'chai';
import { Map } from 'immutable';
import { selectData, selectDomainName, filterData } from '../simple.selectors';
describe('Simple: selectors', () => {
const name = 'simpleName';
const data = [{type: 'free'}, {type:'notFree'}];
const state = {
simpleDomain: Map({name, data}),
};
describe('selectDomainName', () => {
it('should select simple data', () => {
expect(selectDomainName(state)).to.be.equal(name);
});
});
describe('selectSimpleData', () => {
it('should select simple data', () => {
expect(selectData(state)).to.deep.equal(data);
});
});
describe('filterData', () => {
it('should select simple data', () => {
expect(filterData(state)).to.deep.equal([{type: 'free'}]);
});
});
});
WHY TEST?
- Reduce number of bugs
- Tests are good documentation
- Reduce cost of change
- Improve design
- Allow safe refactoring
- Forces you to think
- Makes development faster
- REDUCES FEAR
A study constructed by Microsoft and IBM showed that writing tests can add 15%-35% to development time but reduce the number of bugs by
40%-90%.
Best practices
Best practices
- Use shallow rendering whenever possible.
- Avoid sharing Enzyme wrappers between test cases.
const wrapper = shallow(<Thing name={'Steve'} />);
it('should do stuff', () => {
expect(wrapper).to.doStuff();
});
it('should do different stuff', () => {
expect(wrapper).to.doDifferentStuff();
});
const defaultProps = { name: 'Steve'; };
it('should do stuff', () => {
const wrapper = shallow(<Thing {...defaultProps} />);
expect(wrapper).to.doStuff();
});
it('should do different stuff', () => {
const wrapper = shallow(<Thing {...defaultProps} />);
expect(wrapper).to.doDifferentStuff();
});
Bad
Good
Good
const defaultProps = { name: 'Steve'; };
it('should do stuff', () => {
const wrapper = shallow(<Thing {...defaultProps} />);
expect(wrapper).to.doStuff();
});
it('should do different stuff', () => {
const wrapper = shallow(<Thing {...defaultProps} />);
expect(wrapper).to.doDifferentStuff();
});
Good
// Use helper functions
const defaultProps = { name: 'Steve'; };
const component = props => (
<Thing {...defaultProps} {...props} />;
);
it('should do stuff', () => {
const wrapper = shallow(component());
expect(wrapper).to.doStuff();
});
it('should do different stuff', () => {
const wrapper = shallow(component({name: 'Medziany'});
expect(wrapper).to.doDifferentStuff();
});
Better
Best practices
- Only pass required props when rendering components.
const defaultProps = {
dispatch: () => {},
manifest: { components: [] },
};
it('should render without blowing up', () => {
const wrapper = shallow(<Thing {...defaultProps} />);
expect(wrapper.length).to.eql(1);
});
it('should render with additional props', () => {
const wrapper = shallow(<Thing {...defaultProps} someProp="val" />);
// ...
});
Best practices
- Finding components.
// bad
it('should render checkbox', () => {
const wrapper = shallow(<Component />);
const checkbox = wrapper.find('.checkbox-class');
});
// good
it('should render checkbox', () => {
const wrapper = shallow(<Component />);
const checkbox = wrapper.find(Checkbox);
});
Best practices
- Test inputs and outputs of components. Don’t “reach in” and test the implementation.
Best practices
In React, the input is generally one of three things:
- Static props
- Events generated by the user
- A child component calling a passed-in function
The outputs in React tend to only be three things:
- The presence of a child component
- A call to function prop, like a redux action creator
- Values passed into child component props
Thank you & happy testing!
Sources
Javascript Unit Testing
By Sergey Bolshov
Javascript Unit Testing
- 1,188