A list of recipes to testing your React code
Part of the code and ideas are borrowed from React Testing Cookbook series on egghead.io with awareness of personal choice of tools.
Install babel 6 preset family
npm i babel-preset-es2015 babel-preset-react babel-preset-stage-0 babel-core babel-cli --save-dev
Add .babelrc
{
"presets": ["es2015", "stage-0", "react"]
}
Install testing dependencies
$ npm i tape sinon enzyme react-addons-test-utils babel-tape-runner faucet --save-dev
$ npm i react react-dom --save
- tape - tap-producing test harness for node and browsers
- enzyme - JavaScript Testing utilities for React http://airbnb.io/enzyme/
- react-addons-test-utils - ReactTestUtils makes it easy to test React components in the testing framework of your choice
- babel-tape-runner - Babel + Tape runner for your ESNext code
- faucet - human-readable TAP summarizer
- sinon - Standalone test spies, stubs and mocks for JavaScript.
Lint your ES6 and React code with standard and better test error message with snazzy.
$ npm i standard snazzy --save-dev
In order to make standard understand your ES6
code and JSX
syntax, you may also need to install babel-eslint
and the following to package.json
.
{
"standard": {
"parser": "babel-eslint"
}
}
- standard - π JavaScript Standard Style http://standardjs.com
- snazzy - Format JavaScript Standard Style as Stylish (i.e. snazzy) output
Add scripts
to package.json
{
"scripts": {
"lint": "standard src/**/*.js | snazzy",
"pretest": "npm run lint",
"test": "babel-tape-runner tests/**/*.test.js | faucet"
}
}
- You can start lint with
npm run lint
- Running tests with
npm test
,lint
is part of the test as we defined inpretest
.
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.
Make sure you always import React
to let the test runner know you have JSX syntax in your code.
import React from 'react'
import test from 'tape'
import { shallow } from 'enzyme'
const DummyComponent = (props) => <div>{props.content}</div>
test('Dummy component', assert => {
const msg = 'should render dummy content'
const expected = '<div>dummy content</div>'
const props = {
content: 'dummy content'
}
const $ = shallow(<DummyComponent {...props} />)
const output = $.html()
assert.equal(output, expected, msg)
assert.end()
}))
import fs from 'fs'
import jsdom from 'jsdom'
import resolve from 'resolve'
const jQuery = fs.readFileSync(resolve.sync('jquery'), 'utf-8')
jsdom.env('<!doctype html><html><body></body></html>', {
src: [jQuery]
}, (err, window) => {
console.log('VoilΓ !', window.$('body'))
})
To run your test in the browser with tape-run
A lot of times your test may dependes heavily on the browser and mocking them with jsdom could be troublesome or even impossible. A better solution is to pipe the testing code into the browser, so we could have access to browser only variables like document
, window
.
import test from 'tape'
import React from 'react'
import jQuery from 'jquery'
import { render } from 'react-dom'
test('should have a proper testing environment', assert => {
jQuery('body').append('<input>')
const $searchInput = jQuery('input')
assert.true($searchInput instanceof jQuery, '$searchInput is an instanceof jQuery')
assert.end()
})
And we can run the test with browserify dummyComponent.browser.test.js -t [ babelify --presets [ es2015 stage-0 react ] ] | tape-run
The benefit of this approach is that we don't need to mock anything at all. But there are also downsides of this, first thing is that currently it does not work with enzyme
as it will complain "Cannot find module 'react/lib/ReactContext'". There are also a github issue here.
Secondly, since tape-run
will need to launch an electron application, I'm not sure the performance yet compare to js-dom
. But it really makes the test running in a browser environment easy.
Notes: You need some work to be done to make tape-run(electron) work on Linux.
import { spyLifecycle } from 'enzyme'
// This part inject document and window variable for the DOM mount test
const doc = jsdom.jsdom('<!doctype html><html><body></body></html>')
const win = doc.defaultView
global.document = doc
global.window = win
spyLifecycle(AutosuggestKeyBinderComponent)
let container = doc.createElement('div')
render(<AutosuggestKeyBinderComponent {...props} />, container)
assert.true(AutosuggestKeyBinderComponent.prototype.componentDidMount.calledOnce, 'calls componentDidMount once')
unmountComponentAtNode(container)
assert.true(AutosuggestKeyBinderComponent.prototype.componentWillUnmount.calledOnce, 'calls componentWillUnmount once')
assert.true($.hasClass('myClassName'), msg)
assert.true($.find('.someDOMNode').length, msg)
const expected = props.data.length
assert.equal($.find('.childClass').children().length, expected, msg)
First we prepare a simple React ListComponent
class, the list item will take a handleMouseDown
callback function from props.
// ListComponent
class ListComponent extends React.Component {
constructor (props) {
super(props)
}
render () {
const { user, handleMouseDown } = this.props
return (
<li onMouseDown={handleMouseDown}>{user.name}</li>
)
}
}
export default ListComponent
Than we can start to test it.
import ListComponent from './ListComponent'
import sinon from 'sinon'
// ...
// we spy on the `handleMouseDown` function
const handleMouseDown = sinon.spy()
const props = {
user: {
name: 'fraserxu',
title: 'Frontend Developer'
},
handleMouseDown
}
const $ = shallow(<ListComponent {...props} />)
const listItem = $.find('li')
// emulate the `mouseDown` event
listItem.simulate('mouseDown')
// check if the function get called
const actual = handleMouseDown.calledOnce
const expected = true
assert.equal(actual, expected, msg)
assert.end()
First we prepare a simple React ListComponent
class, the list item will have a custom data attribute data-selected
from props.
// ListComponent
class ListComponent extends React.Component {
constructor (props) {
super(props)
}
render () {
const { user, handleMouseDown, isSelected } = this.props
return (
<li onMouseDown={handleMouseDown} data-selected={isSelected}>>{user.name}</li>
)
}
}
export default ListComponent
This part is a little tricky. As for normal DOM node, we can access the data attribute with $.node.dataset.isSelected
, I tried to get the data attribute for a while and the only solution I found is listItemNode.getAttribute('data-selected')
.
import ListComponent from './ListComponent'
// ...
const noop = () => {}
const props = {
user: {
name: 'fraserxu',
title: 'Frontend Developer'
},
handleMouseDown: noop,
isSelected: true
}
const $ = shallow(<ListComponent {...props} />)
const listItem = $.find('li').node
// here is the trick part
assert.equal(listItemNode.getAttribute('data-selected'), 'true', msg)
assert.end()
Same as expect-jsx, you can use tape-jsx-equal to test JSX strings.
$ npm install --save-dev extend-tape
$ npm install --save-dev tape-jsx-equals
import tape from 'tape'
import addAssertions from 'extend-tape'
import jsxEquals from 'tape-jsx-equals'
const test = addAssertions(tape, { jsxEquals })
assert.jsxEquals(result, <div className='box color-red'></div>)
This is just the beginning of this recipes and is quite limited to the bacis of testing React code. I'll add more along working and learning. If you are interestd to contribue or want to know how to test certain code, send a pull request here or open a Github issue.
Happy testing!
Special thanks to @ericelliott for sharing his knowledge and effort to make our life easier writing JavaScript.
MIT