Functionality Testing
Usability testing
Interface testing
Compatibility testing
Performance testing
Security testing
Unit testing
Integration testing
...
ensure that individual components of the app work as expected. Assertions test the component API.
Source: The Benefits of Unit testing
Source: Unit and integration testing
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.
npm install mocha
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"
}
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”
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
callback:
describe('User', function() {
describe('#save()', function() {
it('should save without error', function(done) {
var user = new User('Luna');
user.save(done);
});
});
});
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);
});
});
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
});
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
});
});
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();
}
});
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
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
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
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 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
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.
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
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);
});
});
});
Action
Reducer
Store
View
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);
});
});
});
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);
});
});
});
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'}]);
});
});
});
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%.
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
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" />);
// ...
});
// 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);
});
In React, the input is generally one of three things:
The outputs in React tend to only be three things:
Sources