REACTIVE MOVEMENT
SETUP
TESTING ENGINE
$ npm install --save-dev mocha
describe('Component', () => {
beforeEach(() => {
// setup env before running tests
});
it('should', () => {
// setup test case expectation
});
afterEach(() => {
// clean env before running tests
});
});
ASSERTION LIBRARY
$ npm install --save-dev chai
import chai, { expect } from 'chai';
describe('Component', () => {
beforeEach(() => {
// setup env before running tests
});
it('should', () => {
expect('racing').to.not.equal('post');
});
afterEach(() => {
// clean env before running tests
});
});
SPY ABILITY
$ npm install --save-dev sinon
$ npm install --save-dev sinon-chai
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('Component', () => {
it('should', () => {
const = callback = sinon.spy();
const proxy = sut(callback);
proxy();
expect(callback).calledWith();
});
});
ENZYME RENDERING
$ npm install --save-dev enzyme
$ npm install --save-dev chai-enzyme
import React from 'react';
import chai, { expect } from 'chai';
import chaiEnzyme from 'chai-enzyme';
import { shallow } from 'enzyme';
import Component from 'path/to/Component/';
chai.use(chaiEnzyme());
describe('Component', () => {
it('should', () => {
const wrapper = shallow(<Component />);
expect(wrapper.find('.class')).be.present();
});
});
RUN CONFIGURATION
$ npm install --save-dev babel-register
{
...
"scripts": {
...
"test": "mocha --compilers js:babel-register --recursive",
"test:watch": "npm test -- --watch"
},
...
}
package.json
CODE
ACTION CREATORS
import * as actions from './ActionTypes';
export function hide() {
return {
type: actions.HIDE_PROMO
};
}
/* Promo actions */
export const HIDE_PROMO = 'HIDE_PROMO';
ActionTypes.js
PromoActions.js
ACTION CREATORS
PromoActions.spec.js
import { expect } from 'chai';
import * as actions from 'path/to/actions/PromoActions';
import * as types from 'path/to/actions/ActionTypes';
describe('Promo actions', () => {
it('should create a hide action', () => {
const expectedAction = {
type: types.HIDE_PROMO
};
expect(actions.hide()).to.deep.equal(expectedAction);
});
});
ASYNC CREATORS
import * as actions from './ActionTypes';
function receiveHorses(horses) {
return {
type: actions.RECEIVE_HORSES,
horses
};
}
export function fetchHorses(url) {
return dispatch => (
fetch(`/horse-tracker/data/${url}`, { credentials: 'include' })
.then(response => (response.json()))
.then(json => (dispatch(receiveHorses(json.data))))
);
}
/* Tracked actions */
export const RECEIVE_HORSES = 'RECEIVE_HORSES';
ActionTypes.js
TrackedActions.js
ASYNC CREATORS
TrackedActions.spec.js
import chai, { expect } from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import fetchMock from 'fetch-mock';
import * as actions from 'path/to/actions/TrackedActions';
import * as types from 'path/to/actions/ActionTypes';
chai.use(sinonChai);
describe('Tracked actions', () => {
let thunk;
const url = 'testUrl';
const fakeData = [
{ testKey: 'testValue' }
];
beforeEach(() => {
thunk = actions.fetchHorses(url);
});
it('should fetch horses data', (done) => {
fetchMock.mock({
name: 'success',
matcher: `/horse-tracker/data/${url}`,
response: {
success: true,
data: fakeData
}
});
const dispatchSpy = sinon.spy();
const expectedAction = {
type: types.RECEIVE_HORSES,
horses: fakeData
};
thunk(dispatchSpy)
.then(() => {
expect(dispatchSpy).calledWith(expectedAction);
})
.then(done)
.catch(done);
});
});
REDUCERS
import * as actions from 'path/to/actions/ActionTypes';
export default function promoReducer(state = {}, action) {
switch (action.type) {
case actions.HIDE_PROMO:
return {
...state,
isHidden: true
};
default:
return state;
}
}
/* Promo actions */
export const HIDE_PROMO = 'HIDE_PROMO';
ActionTypes.js
PromoReducer.js
REDUCERS
PromoReducer.spec.js
import { expect } from 'chai';
import reducer from 'path/to/reducers/PromoReducer';
import * as types from 'path/to/actions/ActionTypes';
describe('Promo reducer', () => {
it('should return the initial state by default', () => {
expect(
reducer(undefined, {})
).to.deep.equal(
{}
);
});
it('should handle HIDE_PROMO action type', () => {
expect(
reducer({}, {
type: types.HIDE_PROMO
})
).to.deep.equal({
isHidden: true
});
});
});
COMPONENTS
import './styles.scss';
import React, { PropTypes } from 'react';
import Svg from 'svg';
const Promo = ({ promo, onHide }) => (
promo.isHidden ? null : (
<div className="ht-promo cf">
<img
src="img/promo.png"
alt="Horse Tracker Promo Message"
className="ht-promo__image"
/>
<div className="ht-promo__title">Never miss a trick</div>
<span className="ht-promo__close" onClick={onHide}>
<Svg
iconName="crossFull"
size="Large"
/>
</span>
</div>
)
);
Promo.propTypes = {
promo: PropTypes.object.isRequired,
onHide: PropTypes.func.isRequired
};
export default Promo;
PromoComponent.js
CONTAINERS
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import * as actions from 'path/to/actions/TrackedActions';
import Tracked from 'path/to/components/Tracked/';
export class TrackedContainer extends Component {
componentDidMount() {
const { fetchHorses } = this.props;
fetchHorses('tracked');
}
render() {
const { tracked } = this.props;
return (
<Tracked tracked={tracked} />
);
}
}
TrackedContainer.propTypes = {
tracked: PropTypes.object.isRequired,
fetchHorses: PropTypes.func.isRequired
};
const mapStateToProps = ({ tracked }) => ({ tracked });
export default connect(
mapStateToProps,
{ fetchHorses: actions.fetchHorses }
)(TrackedContainer);
TrackedContainer.js
CONTAINERS
import React from 'react';
import chai, { expect } from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import proxyquire from 'proxyquire';
chai.use(sinonChai);
describe('Tracked container', () => {
let TrackedContainer;
let reactRedux;
let wrapWithConnect;
beforeEach(() => {
wrapWithConnect = sinon.stub().returns({});
reactRedux = {
connect: sinon.stub().returns(wrapWithConnect)
};
const TrackedActions = {
fetchHorses: sinon.spy()
};
const Tracked = {
default: Symbol()
};
TrackedContainer = proxyquire('path/to/containers/Tracked', {
'react-redux': reactRedux,
'path/to/actions/TrackedActions': TrackedActions,
'path/to/components/Tracked/': Tracked,
react: React
}).TrackedContainer;
});
it('should fetch horses on component mounted', () => {
const fetchHorses = sinon.spy();
const sut = new TrackedContainer();
sut.props = {
fetchHorses
};
sut.componentDidMount();
expect(fetchHorses).calledWith('tracked');
});
});
TrackedContainer.spec.js
KEEP GOING
Reactive Movement
By Roman Stremedlovskyi
Reactive Movement
Test-Driven Development in Reactive projects
- 1,232