├── .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) =>
{props.content}
89 | 90 | test('Dummy component', assert => { 91 | const msg = 'should render dummy content' 92 | 93 | const expected = '
dummy content
' 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 |
  • {user.name}
  • 12 | ) 13 | } 14 | } 15 | 16 | ListComponent.propTypes = { 17 | user: PropTypes.object.isRequired, 18 | handleMouseDown: PropTypes.func.isRequired 19 | } 20 | 21 | export default ListComponent 22 | -------------------------------------------------------------------------------- /tests/dummy.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import test from 'tape' 3 | import { shallow } from 'enzyme' 4 | 5 | const DummyComponent = (props) =>
    {props.content}
    6 | 7 | test('Dummy component', assert => { 8 | const msg = 'should render dummy content' 9 | 10 | const expected = '
    dummy content
    ' 11 | 12 | const props = { 13 | content: 'dummy content' 14 | } 15 | 16 | const $ = shallow() 17 | const output = $.html() 18 | 19 | assert.equal(output, expected, msg) 20 | 21 | assert.end() 22 | }) 23 | -------------------------------------------------------------------------------- /tests/listComponent.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import test from 'tape' 3 | import { shallow } from 'enzyme' 4 | import sinon from 'sinon' 5 | 6 | import ListComponent from '../src/ListComponent' 7 | 8 | test('ListComponent component', assert => { 9 | const msg = 'should react to mouseEvent' 10 | 11 | const handleMouseDown = sinon.spy() 12 | const props = { 13 | user: { 14 | name: 'fraserxu', 15 | title: 'Frontend Developer' 16 | }, 17 | handleMouseDown 18 | } 19 | const $ = shallow() 20 | const listItem = $.find('li') 21 | 22 | // emulate the `mouseDown` event 23 | listItem.simulate('mouseDown') 24 | 25 | // check if the function get called 26 | const actual = handleMouseDown.calledOnce 27 | const expected = true 28 | 29 | assert.equal(actual, expected, msg) 30 | assert.end() 31 | }) 32 | --------------------------------------------------------------------------------