These are my notes from the Egghead.io course "React Testing Cookbook".
npm install --save-dev mocha expect react-addon-test-utils
(Not chai
, hmm...)
- In
package.json
, addtest
underscripts
:
mocha './src/**/*.spec.js' --compilers js:babel-core/register
- In a spec file, we use
mocha
andexpect
:
import expect from 'expect';
describe('emtpy', () => {
it('should work', () => {
expect(true).toEqual(true);
});
});
We have a hypothetical utility function (not React related) called createId
, which generates unique IDs for a quote-keeper app.
describe('createId', () => {
it('should convert a description into a unique id', () => {
const actual = createId(123, 'Cool example');
const expected = '123-cool-example';
expect(actual).toEqual(expected);
});
});
import React from 'react';
import TestUtils from 'react-addon-test-utils';
import expect from 'expect';
const CoolComponent = ({greeting}) => (
<div>
<h1>Greeting</h1>
<div>{greeting}</h1>
</div>
);
describe('CoolComponent', () => {
it('should...', () => {
// shallow rendering means only one component level deep
const renderer = TestUtils.createRenderer();
// same as ReactDOM.render()
renderer.render(<CoolComponent greeting='hello world' />);
// object output of shallow render
const output = renderer.getRenderOutput();
console.log(output);
});
});
import React from 'react';
import TestUtils from 'react-addon-test-utils';
import expect from 'expect';
const CoolComponent = ({greeting}) => (
<div>
<h1>Greeting</h1>
<div>{greeting}</h1>
</div>
);
describe('CoolComponent', () => {
it('should render the greeting', () => {
const renderer = TestUtils.createRenderer();
renderer.render(<CoolComponent greeting='hello world' />);
const actual = renderer.getRenderOutput();
const expected = (
<div>
<h1>Greeting</h1>
<div>hello world</div>
</div>
);
expect(actual).toEqual(expected)
});
})
- What about when the test fails? We get a giant diff dump of internal react structure. We need better diffing of JSX output.
We can get this with expect-jsx.
Some things we can do with expect-jsx
: toEqualJSX
, toNotEqualJSX
, toIncludeJSX
(recursive children search). This last one means you don't have to tightly couple test-selectors to your HTML structure.
npm install --save-dev expect-jsx
import React from 'react';
import TestUtils from 'react-addon-test-utils';
import expect from 'expect';
import expectJSX from 'expect-jsx';
expect.extend(expectJSX);
const CoolComponent = ({greeting}) => (
<div>
<h1>Greeting</h1>
<div>{greeting}</h1>
</div>
);
describe('CoolComponent', () => {
it('should render the greeting', () => {
const renderer = TestUtils.createRenderer();
renderer.render(<CoolComponent greeting='hello world' />);
const actual = renderer.getRenderOutput();
const expected = <div>hello world</div>;
expect(actual).toIncludeJSX(expected);
});
})
This is about using the type
attribute of getRenderOutput()
. We can assert that the rendered output is a certain type of tag.
import React from 'react';
import TestUtils from 'react-addon-test-utils';
import expect from 'expect';
import LikeCounter from './LikeCounter';
describe('LikeCounter', () => {
it('should be a link', () => {
const renderer = TestUtils.createRenderer();
renderer.render(<LikeCounter count={5} />);
const actual = renderer.getRenderOutput().type;
const expected = 'a';
expect(actual).toEqual(expected);
});
});
We want to write tests to ensure our icons are rendering correctly.
import React from 'react';
import TestUtils from 'react-addon-test-utils';
import expect from 'expect';
import Icon from './Icon';
describe('Icon', () => {
it('should render the icon', () => {
const renderer = TestUtils.createRenderer();
renderer.render(<Icon name='facebook' />);
// `includes` is an ES6 String.prototype function
const actual = renderer.getRenderOutput().props.className.includes('facebook');
const expected = true;
expect(actual).toEqual(expected);
});
});
Same idea as #7, but with a conditional on the component (isActive=[bool]
). Test both true and false.
import React from 'react';
import TestUtils from 'react-addon-test-utils';
import expect from 'expect';
import LikeCounter from './LikeCounter';
describe('LikeCounter', () => {
it('should show the like count as active', () => {
const renderer = TestUtils.createRenderer();
renderer.render(<LikeCounter count={5} isActive={true} />);
const actual = renderer.getRenderOutput().props.className.includes('LikeCounter--active');
const expected = true;
expect(actual).toEqual(expected);
});
it('should show the like count as inactive', () => {
const renderer = TestUtils.createRenderer();
renderer.render(<LikeCounter count={5} isActive={false} />);
const actual = renderer.getRenderOutput().props.className.includes('LikeCounter--active');
const expected = false;
expect(actual).toEqual(expected);
});
});
Let's refactor #8 using some shared logic in the describe block. He calls it a factory function. OK.
import React from 'react';
import TestUtils from 'react-addon-test-utils';
import expect from 'expect';
import LikeCounter from './LikeCounter';
describe('LikeCounter', () => {
function renderLikeCounter(isActive) {
const renderer = TestUtils.createRenderer();
renderer.render(<LikeCounter count={5} isActive={isActive} />);
return renderer
.getRenderOutput()
.props
.className
.includes('LikeCounter--active');
}
describe('isActive', () => {
it('should show the like count as active', () => {
expect(renderLikeCounter(true)).toEqual(true);
});
it('should show the like count as inactive', () => {
expect(renderLikeCounter(false)).toEqual(false);
});
});
});
import React from 'react';
import TestUtils from 'react-addon-test-utils';
import expect from 'expect';
import expectJSX from 'expect-jsx';
expect.extend(expectJSX);
import LikeCounter from './LikeCounter';
describe('LikeCounter', () => {
it('should render like counts', () => {
const renderer = TestUtils.createRenderer();
renderer.render(<LikeCounter count={5} />);
// const children = renderer.getRenderOutput().props.children;
// We could just keep chaining .props.children ... on and on
// But this is ugly. How else to do it? (A: toIncludeJSX)
const expected = '5 likes';
const actual = renderer.getRenderOutput();
expect(actual).toIncludeJSX(expected);
});
});
In this sort-of-integration-test we dispatch multiple actions, and only make one assertion at the end to verify the final state.
import { store } from './store';
import expect from 'expect';
describe('store', () => {
it('should work with a series of actions', () => {
const actions = [
{
type: 'ADD_QUOTE_BY_ID',
payload: {
text: 'The best way to cheer yourself up is to try to cheer someone else up.',
author: 'Mark Twain',
id: 1,
likeCount: 24
}
},
{
type: 'ADD_QUOTE_BY_ID',
payload: {
text: 'Whatever you are, be a good one.',
author: 'Abraham Lincoln',
id: 2,
likeCount: 0
}
},
{
type: 'REMOVE_QUOTE_BY_ID',
payload: { id: 1 }
},
{
type: 'LIKE_QUOTE_BY_ID',
payload: { id: 2 }
},
{
type: 'LIKE_QUOTE_BY_ID',
payload: { id: 2 }
},
{
type: 'UNLIKE_QUOTE_BY_ID',
payload: { id: 2 }
},
{
type: 'UPDATE_THEME_COLOR',
payload: { color: '#777777' }
}
];
actions.forEach(action => store.dispatch(action));
const actual = store.getState();
const expected = {
quotes: [
{
text: 'Whatever you are, be a good one.',
author: 'Abraham Lincoln',
id: 2,
likeCount: 1
}
],
theme: {
color: '#777777'
}
};
expect(actual).toEqual(expected);
});
});
In Redux, reducers must provide an initial/default state.
import { store } from './store';
import expect from 'expect';
describe('store', () => {
it('should initialize', () => {
const actual = store.getState();
const expected = {
quotes: [],
theme: { color: '#ffffff' }
};
expect(actual).toEqual(expected);
});
});
import expect from 'expect';
import themeReducer from './themeReducer';
describe('themeReducer', () => {
function stateBefore() {
return {
color: '#ffffff'
};
}
const action = {
type: 'UPDATE_THEME_COLOR',
payload: { color: '#56ddff' }
}
it('should change the theme color', () => {
const actual = themeReducer(stateBefore(), action)
const actual = store.getState();
const expected = { color: '#56ddff' }
expect(actual).toEqual(expected);
});
});
The use of stateBefore
as a function is overkill here since it's just a single action. Let's try another reducer to go a bit further:
import expect from 'expect';
import quoteReducer from './quoteReducer';
describe('quoteReducer', () => {
function stateBefore() {
return [
{
text: 'Lorem ipsum',
author: 'Jane Doe',
id: 1,
likeCount: 7
},
{
text: 'Ullamco laboris nisi ut aliquip',
author: 'John Smith',
id: 2,
likeCount: 0
},
]
};
it('should add quotes by id', () => {
const action = {
type: 'ADD_QUOTE_BY_ID',
payload: {
text: 'This is a new quote',
author: 'Someone awesome',
id: 3,
likeCount: 0
}
}
const actual = quoteReducer(stateBefore(), action)
const expected = [
{
text: 'Lorem ipsum',
author: 'Jane Doe',
id: 1,
likeCount: 7
},
{
text: 'Ullamco laboris nisi ut aliquip',
author: 'John Smith',
id: 2,
likeCount: 0
},
{
text: 'This is a new quote',
author: 'Someone awesome',
id: 3,
likeCount: 0
}
];
expect(actual).toEqual(expected);
});
it('should return prev state when trying to make likeCount negative', () => {
const action = {
type: 'UNLIKE_QUOTE_BY_ID',
payload: { id: 2 }
};
const actual = quoteReducer(stateBefore(), action);
const expected = stateBefore();
expect(actual).toEqual(expected);
});
});