- Test runner: ava
- React components testing: enzyme
- Endpoint testing: express + supertest
- Mocking framework: sinon
- External dependencies mocking: proxyquire
all the following examples are using ava syntax, but they may be easily adapted to mocha or tape as well
import test from 'ava';
import React from 'react';
import {shallow} from 'enzyme';
import Component from '../component';
// avoid using beforeEach
// all mocks should be provided by a factory function with consistent naming
function getComponentMock(opts) {
const defaultOpts = {
// props should be a separate property inside options, otherwise it
// becomes hard to differentiate them from other options
props: {
bar: 2
},
children: ['foo']
}
const options = {...defaultOpts, ...opts);
// shallow render should be preferred
return shallow(
<Component {...options.props}>
{options.children}
</Component>
);
}
test('component should have bar set to 1', t => {
const component = getComponentMock({
props: {
bar: 1
}
});
t.is(component.prop('bar'), 1);
});
test('clicks should work', t => {
// needed spies are created per test
const onButtonClick = sinon.spy();
const component = getComponentMock({
props: {
onButtonClick
}
});
component.find('button').simulate('click');
t.true(onButtonClick.calledOnce);
});
import test from 'ava';
import sinon from 'sinon';
import React from 'react';
import { mount } from 'enzyme';
import { jsdom } from 'jsdom';
import Component from '../component';
function getComponentMock(opts = {}) {
const defaultOpts = {
// pass fakeDom as an option allow to use resulting dom state
// from other components operations or multiple components rendering
fakeDom: jsdom('<body></body>'),
props: { bar: 1 },
};
const options = {...defaultOpts, ...opts};
global.document = options.fakeDom;
global.window = document.defaultView;
global.navigator = window.navigator;
return {
component: mount(<Component {...options.props} />)
};
}
test('some global is defined', t => {
const {component} = getComponentMock();
//assuming the component write the global when mounted
t.is(window.__MY_DIRTY_GLOBAL__, 'EXPECTED');
});
test('multiple components', t => {
const {component : component1} = getComponentMock();
const {component : component2} = getComponentMock({
// using the DOM state resulting from component1 rendering
fakeDom: window.document
});
t.is(window.__MY_DIRTY_GLOBAL__, 'EXPECTED');
});
import test from 'ava';
import express from 'express';
import request from 'supertest';
// sample data should be in sync with official service API, but only contains keys
// that we need, this way it would be easier to have a reference to stuff that must
// remain retrocompatible on those services
import serviceGetRouteSample from 'serviceGetRouteSample.json';
function getServiceMock(opts) {
const defaultOpts = {
handlers: {
getRoute: (req, res) => {res.json(serviceGetRouteSample)}
}
}
const options = {...defaultOpts, ...opts);
// we always create a brand new server instance to allow parallel testing
const api = express();
api.get('/route', opts.handlers.getRoute);
api.post('/route', opts.handlers.postRoute);
return api
}
test('service:Success', async t => {
t.plan(2);
//spies may be passed as handlers
const service = getServiceMock();
const res = await request(service)
.post('/route')
.send({foo: 'bar'});
t.is(res.status, 200);
t.is(res.body.email, 'bar');
});
import test from 'ava';
import React from 'react';
import {shallow} from 'enzyme';
import request from 'supertest';
test('fetching data for component', async t => {
const service = getServiceMock();
// we could directly pass some mocked json here, but I think it would be
// better to have a single implementation for any mocked service and having
// it to behave as a “real” server especially if it is going to be used
// more than once
const res = await request(service)
.get('route');
const component = getComponentMock({
props: {
data: res.body
}
});
t.is(component.prop('data'), expectedData);
});
import test from 'ava';
import proxyquire from 'proxyquire';
// ensure we don't get any module from the cache, but to load it fresh every time
proxyquire.noPreserveCache();
// never import the module to be tested globally, always use a factory function
// to get the proxyquire version of it
function getModuleMock() {
const dep1 = sinon.stub();
const dep2 = {
func: sinon.stub()
};
const module = proxyquire('module', {
'./dep1': dep1,
'../utils/dep2': dep2
});
return {
module,
dep1,
dep2
};
}
test('external dependency should work', t => {
// the module itself should be exported in the first position
// note that if you do not need any dependency stub, but just the module
// you can simply do const {module} = getModuleMock();
const {
module,
dep1,
dep2
} = getModuleMock();
// test specific behavior of a dependency must be local to the test itself
// but if it never change and is really obvious it may be defined on the factory
dep2.func.withArgs('foo').yields('bar');
const test = module.doSomething();
t.true(dep1.calledOnce());
t.true(dep2.func.calledOnce());
t.is(test, 'bar');
}
import test from 'ava';
import sinon from 'sinon';
import proxyquire from 'proxyquire';
// ensure we don't get any module from the cache, but to load it fresh every time
proxyquire.noPreserveCache();
async function testAsyncAction(action, state) {
const fakeDispatch = sinon.spy();
const fakeGetState = () => state;
await action(fakeDispatch, fakeGetState);
return fakeDispatch;
}
// fake module pattern with mock dependencies
function getActionsMock() {
const fetch = sinon.stub();
const actions = proxyquire('../actions', {
'fetch': fetch
});
return {
actions,
fetch
};
}
t('some action', t => {
const {actions} = getActionsMock();
const action = actions.someAction(args);
const expectedAction = {
type: 'SOME_ACTION',
args: 'SOME_ARGS'
}
t.deepEqual(action, expectedAction);
})
t('some async action', async t => {
const {
actions,
fetch
} = getActionsMock();
const state = {
data: 'some initial data'
}
fetch.returns(Promise.resolve('fake data'))
// use try/catch if error handling is needed
const dispatch = await testAsyncAction(actions.someAction(), state)
t.true(dispatch.calledWith(expectedAction));
})