├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── src
└── ListComponent.js
└── tests
├── dummy.test.js
└── listComponent.test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0", "react"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Fraser Xu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | react-testing-recipes
2 | =====================
3 |
4 | A list of recipes to testing your React code
5 |
6 | Part of the code and ideas are borrowed from **React Testing Cookbook** series on [egghead.io](https://egghead.io) with awareness of personal choice of tools.
7 |
8 | Setup
9 | -----
10 |
11 | Install babel 6 preset family
12 |
13 | ```
14 | npm i babel-preset-es2015 babel-preset-react babel-preset-stage-0 babel-core babel-cli --save-dev
15 | ```
16 |
17 | Add `.babelrc`
18 |
19 | ```JSON
20 | {
21 | "presets": ["es2015", "stage-0", "react"]
22 | }
23 |
24 | ```
25 |
26 | Install testing dependencies
27 |
28 | ```
29 | $ npm i tape sinon enzyme react-addons-test-utils babel-tape-runner faucet --save-dev
30 | $ npm i react react-dom --save
31 | ```
32 | * **tape** - tap-producing test harness for node and browsers
33 | * **enzyme** - JavaScript Testing utilities for React http://airbnb.io/enzyme/
34 | * **react-addons-test-utils** - ReactTestUtils makes it easy to test React components in the testing framework of your choice
35 | * **babel-tape-runner** - Babel + Tape runner for your ESNext code
36 | * **faucet** - human-readable TAP summarizer
37 | * **sinon** - Standalone test spies, stubs and mocks for JavaScript.
38 |
39 | Lint your ES6 and React code with [standard](https://github.com/feross/standard) and better test error message with snazzy.
40 |
41 | ```
42 | $ npm i standard snazzy --save-dev
43 | ```
44 | 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`.
45 |
46 | ```JSON
47 | {
48 | "standard": {
49 | "parser": "babel-eslint"
50 | }
51 | }
52 | ```
53 | * **standard** - :star2: JavaScript Standard Style http://standardjs.com
54 | * **snazzy** - Format JavaScript Standard Style as Stylish (i.e. snazzy) output
55 |
56 | Add `scripts` to `package.json`
57 |
58 | ```JSON
59 | {
60 | "scripts": {
61 | "lint": "standard src/**/*.js | snazzy",
62 | "pretest": "npm run lint",
63 | "test": "babel-tape-runner tests/**/*.test.js | faucet"
64 | }
65 | }
66 | ```
67 |
68 | Running test
69 | ------------
70 |
71 | * You can start lint with `npm run lint`
72 | * Running tests with `npm test`, `lint` is part of the test as we defined in `pretest`.
73 |
74 | What to test
75 | ----------------
76 |
77 | #### Shallow Rendering
78 |
79 | > 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.
80 |
81 | Make sure you always import `React` to let the test runner know you have JSX syntax in your code.
82 |
83 | ```JavaScript
84 | import React from 'react'
85 | import test from 'tape'
86 | import { shallow } from 'enzyme'
87 |
88 | const DummyComponent = (props) =>
'
94 |
95 | const props = {
96 | content: 'dummy content'
97 | }
98 |
99 | const $ = shallow()
100 | const output = $.html()
101 |
102 | assert.equal(output, expected, msg)
103 |
104 | assert.end()
105 | }))
106 | ```
107 |
108 | #### To build a jQuery ready JSDOM env
109 |
110 | ```JavaScript
111 | import fs from 'fs'
112 | import jsdom from 'jsdom'
113 | import resolve from 'resolve'
114 |
115 | const jQuery = fs.readFileSync(resolve.sync('jquery'), 'utf-8')
116 |
117 | jsdom.env('', {
118 | src: [jQuery]
119 | }, (err, window) => {
120 | console.log('Voilà!', window.$('body'))
121 | })
122 | ```
123 |
124 | #### To run your test in the browser with [tape-run](https://github.com/juliangruber/tape-run)
125 |
126 | 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`.
127 |
128 | ```JavaScript
129 | import test from 'tape'
130 | import React from 'react'
131 | import jQuery from 'jquery'
132 | import { render } from 'react-dom'
133 |
134 | test('should have a proper testing environment', assert => {
135 | jQuery('body').append('')
136 | const $searchInput = jQuery('input')
137 |
138 | assert.true($searchInput instanceof jQuery, '$searchInput is an instanceof jQuery')
139 |
140 | assert.end()
141 | })
142 | ```
143 |
144 | And we can run the test with `browserify dummyComponent.browser.test.js -t [ babelify --presets [ es2015 stage-0 react ] ] | tape-run`
145 |
146 | 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](https://github.com/airbnb/enzyme/issues/47).
147 |
148 | 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.
149 |
150 | **Notes:** You [need some work to be done](https://github.com/juliangruber/tape-run/issues/32) to make tape-run(electron) work on Linux.
151 |
152 | #### Test component life cycle
153 |
154 | ```JavaScript
155 | import { spyLifecycle } from 'enzyme'
156 |
157 | // This part inject document and window variable for the DOM mount test
158 | const doc = jsdom.jsdom('')
159 | const win = doc.defaultView
160 | global.document = doc
161 | global.window = win
162 |
163 | spyLifecycle(AutosuggestKeyBinderComponent)
164 |
165 | let container = doc.createElement('div')
166 | render(, container)
167 |
168 | assert.true(AutosuggestKeyBinderComponent.prototype.componentDidMount.calledOnce, 'calls componentDidMount once')
169 |
170 | unmountComponentAtNode(container)
171 |
172 | assert.true(AutosuggestKeyBinderComponent.prototype.componentWillUnmount.calledOnce, 'calls componentWillUnmount once')
173 | ```
174 |
175 | #### Check a component has certain className
176 |
177 | ```JavaScript
178 | assert.true($.hasClass('myClassName'), msg)
179 | ```
180 |
181 | #### Check a DOM node exist
182 |
183 | ```JavaScript
184 | assert.true($.find('.someDOMNode').length, msg)
185 | ```
186 |
187 | #### Check a component has child element
188 |
189 | ```JavaScript
190 | const expected = props.data.length
191 | assert.equal($.find('.childClass').children().length, expected, msg)
192 | ```
193 | #### Emulate mouse event
194 |
195 | First we prepare a simple React `ListComponent` class, the list item will take a `handleMouseDown` callback function from props.
196 |
197 | ```JavaScript
198 | // ListComponent
199 | class ListComponent extends React.Component {
200 | constructor (props) {
201 | super(props)
202 | }
203 |
204 | render () {
205 | const { user, handleMouseDown } = this.props
206 | return (
207 |
{user.name}
208 | )
209 | }
210 | }
211 |
212 | export default ListComponent
213 | ```
214 |
215 | Than we can start to test it.
216 |
217 | ```JavaScript
218 | import ListComponent from './ListComponent'
219 | import sinon from 'sinon'
220 |
221 | // ...
222 |
223 | // we spy on the `handleMouseDown` function
224 | const handleMouseDown = sinon.spy()
225 | const props = {
226 | user: {
227 | name: 'fraserxu',
228 | title: 'Frontend Developer'
229 | },
230 | handleMouseDown
231 | }
232 | const $ = shallow()
233 | const listItem = $.find('li')
234 |
235 | // emulate the `mouseDown` event
236 | listItem.simulate('mouseDown')
237 |
238 | // check if the function get called
239 | const actual = handleMouseDown.calledOnce
240 | const expected = true
241 |
242 | assert.equal(actual, expected, msg)
243 | assert.end()
244 | ```
245 |
246 | #### Test custom data-attribute
247 |
248 | First we prepare a simple React `ListComponent` class, the list item will have a custom data attribute `data-selected` from props.
249 |
250 | ```JavaScript
251 | // ListComponent
252 | class ListComponent extends React.Component {
253 | constructor (props) {
254 | super(props)
255 | }
256 |
257 | render () {
258 | const { user, handleMouseDown, isSelected } = this.props
259 | return (
260 |
>{user.name}
261 | )
262 | }
263 | }
264 |
265 | export default ListComponent
266 | ```
267 |
268 | This part is a little tricky. As for normal DOM node, we can [access the data attribute](https://developer.mozilla.org/en/docs/Web/Guide/HTML/Using_data_attributes) 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')`.
269 |
270 | ```JavaScript
271 | import ListComponent from './ListComponent'
272 |
273 | // ...
274 |
275 | const noop = () => {}
276 | const props = {
277 | user: {
278 | name: 'fraserxu',
279 | title: 'Frontend Developer'
280 | },
281 | handleMouseDown: noop,
282 | isSelected: true
283 | }
284 | const $ = shallow()
285 | const listItem = $.find('li').node
286 |
287 | // here is the trick part
288 | assert.equal(listItemNode.getAttribute('data-selected'), 'true', msg)
289 | assert.end()
290 | ```
291 |
292 | #### Test JSX equal with `tape-jsx-equals`
293 |
294 | Same as [expect-jsx](https://github.com/algolia/expect-jsx), you can use [tape-jsx-equal](https://www.npmjs.com/package/tape-jsx-equals) to test JSX strings.
295 |
296 | ```
297 | $ npm install --save-dev extend-tape
298 | $ npm install --save-dev tape-jsx-equals
299 | ```
300 |
301 | ```JavaScript
302 | import tape from 'tape'
303 | import addAssertions from 'extend-tape'
304 | import jsxEquals from 'tape-jsx-equals'
305 |
306 | const test = addAssertions(tape, { jsxEquals })
307 |
308 | assert.jsxEquals(result, )
309 | ```
310 |
311 | Roadmap
312 | ---------
313 |
314 | 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.
315 |
316 | Happy testing!
317 |
318 | Further readings
319 | ---------------
320 |
321 | * [Why I use Tape Instead of Mocha & So Should You](https://medium.com/javascript-scene/why-i-use-tape-instead-of-mocha-so-should-you-6aa105d8eaf4)
322 | * [A pure component dev starter kit for React.](https://github.com/ericelliott/react-pure-component-starter)
323 |
324 | Special thanks to [@ericelliott](https://github.com/ericelliott) for sharing his knowledge and effort to make our life easier writing JavaScript.
325 |
326 | ### License
327 | MIT
328 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-testing-recipes",
3 | "version": "1.0.0",
4 | "description": "A list of recipes to testing your React code",
5 | "main": "index.js",
6 | "scripts": {
7 | "lint": "standard src/**/*.js | snazzy",
8 | "pretest": "npm run lint",
9 | "test": "babel-tape-runner tests/**/*.test.js | faucet"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/fraserxu/react-testing-recipes.git"
14 | },
15 | "keywords": [
16 | "react",
17 | "testing",
18 | "recipes"
19 | ],
20 | "author": "Fraser Xu",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/fraserxu/react-testing-recipes/issues"
24 | },
25 | "homepage": "https://github.com/fraserxu/react-testing-recipes#readme",
26 | "devDependencies": {
27 | "babel-cli": "^6.3.17",
28 | "babel-core": "^6.3.26",
29 | "babel-preset-es2015": "^6.3.13",
30 | "babel-preset-react": "^6.3.13",
31 | "babel-preset-stage-0": "^6.3.13",
32 | "babel-tape-runner": "^2.0.0",
33 | "enzyme": "^1.2.0",
34 | "faucet": "0.0.1",
35 | "react": "^0.14.5",
36 | "react-addons-test-utils": "^0.14.5",
37 | "sinon": "^1.17.2",
38 | "snazzy": "^2.0.1",
39 | "standard": "^5.4.1",
40 | "tape": "^4.4.0"
41 | },
42 | "dependencies": {
43 | "react": "^0.14.5",
44 | "react-dom": "^0.14.5"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/ListComponent.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 |
3 | class ListComponent extends React.Component {
4 | constructor (props) {
5 | super(props)
6 | }
7 |
8 | render () {
9 | const { user, handleMouseDown } = this.props
10 | return (
11 |