├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .jshintignore ├── .jshintrc ├── README.md ├── app ├── components │ ├── AddTodo.jsx │ ├── App.jsx │ ├── TodoItem.jsx │ └── TodoList.jsx ├── img │ ├── demo-delete.png │ ├── demo-newitem.png │ ├── demo-toggle.png │ ├── demo.png │ └── react.png ├── index.html ├── main.css ├── main.jsx └── stores │ └── TodoStore.js ├── package.json ├── test ├── dom1.test.js ├── dom2.test.js ├── dom3.test.js ├── enzyme1.test.js ├── setup.js ├── shallow1.test.js └── shallow2.test.js ├── webpack.config.js └── webpack.production.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015", "stage-0", "react"], 3 | 4 | /* if you want to use babel runtime, uncomment the following line */ 5 | // "plugins": ["transform-runtime"], 6 | 7 | "env": { 8 | "build": { 9 | "optional": ["optimisation", "minification"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | **/*.css 3 | **/*.html 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | ecmaFeatures: { 6 | jsx: true 7 | }, 8 | "globals": { 9 | }, 10 | "plugins": [ 11 | ], 12 | "extends": "eslint-config-airbnb", 13 | "rules": { 14 | "comma-dangle": 0, 15 | "no-console": 0, 16 | "id-length": 0, 17 | "react/prop-types": 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | 4 | "curly": true, 5 | "latedef": true, 6 | "quotmark": true, 7 | "undef": true, 8 | "unused": true, 9 | "trailing": true 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo shows you how to test React component. It is loosely based on Jack Franklin's article ["Testing React Applications"](http://12devsofxmas.co.uk/2015/12/day-2-testing-react-applications/). 2 | 3 | ![](app/img/react.png) 4 | 5 | ## Demo 6 | 7 | ```bash 8 | $ git clone https://github.com/ruanyf/react-testing-demo.git 9 | $ cd react-testing-demo && npm install 10 | $ npm start 11 | $ open http://127.0.0.1:8080 12 | ``` 13 | 14 | Now, you visit http://127.0.0.1:8080/, and should see a Todo app. 15 | 16 | ![](app/img/demo.png) 17 | 18 | There are 5 places to test. 19 | 20 | > 1. App's title should be "Todos" 21 | > 1. Initial state of a Todo item should be right ("done" or "undone") 22 | > 1. Click a Todo item, its state should be toggled (from "undone" to "done", or vice versa) 23 | > 1. Click a Delete button, the Todo item should be deleted 24 | > 1. Click the Add Todo button, a new Todo item should be added into the TodoList 25 | 26 | All [test cases](https://github.com/ruanyf/react-testing-demo/tree/master/test) have been written. You run `npm test` to find the test result. 27 | 28 | ```bash 29 | $ npm test 30 | ``` 31 | 32 | ## Index 33 | 34 | - [Testing Library](#testing-library) 35 | - [React official Test Utilities](#react-official-test-utilities) 36 | - [Shallow Rendering](#shallow-rendering) 37 | - [renderIntoDocument](#renderintodocument) 38 | - [findDOMNode](#finddomnode) 39 | - [Enzyme Library](#enzyme-library) 40 | - [shallow](#shallow) 41 | - [render](#render) 42 | - [mount](#mount) 43 | - [API List](#api-list) 44 | - [License](#license) 45 | 46 | ## Testing Library 47 | 48 | The most important tool of testing React is [official Test Utilities](https://facebook.github.io/react/docs/test-utils.html), but it only provides low-level API. As a result, some third-party test libraries are built based on it. Airbnb's [Enzyme library](https://github.com/airbnb/enzyme) is the easiest one to use among them. 49 | 50 | Thus every test case has at least two ways to write. 51 | 52 | > - Test Utilities' way 53 | > - Enzyme's way 54 | 55 | This repo will show you both of them. 56 | 57 | ## React official Test Utilities 58 | 59 | Since a component could be rendered into either a virtual DOM object (`React.Component`'s instance) or a real DOM node, [Test Utilities](https://facebook.github.io/react/docs/test-utils.html) library gives you two testing choices. 60 | 61 | > - **Shallow Rendering**: testing a virtual DOM object 62 | > - **DOM Rendering**: testing a real DOM node 63 | 64 | ### Shallow Rendering 65 | 66 | [Shallow Rendering](https://facebook.github.io/react/docs/test-utils.html#shallow-rendering) just renders a component "one level deep" without worrying about the behavior of child components, and returns a virtual DOM object. It does not require a DOM, since the component will not be mounted into DOM. 67 | 68 | At first, import the Test Utilities in your test case script. 69 | 70 | ```javascript 71 | import TestUtils from 'react-addons-test-utils'; 72 | ``` 73 | 74 | Then, write a Shallow Rendering function. 75 | 76 | ```javascript 77 | import TestUtils from 'react-addons-test-utils'; 78 | 79 | function shallowRender(Component) { 80 | const renderer = TestUtils.createRenderer(); 81 | renderer.render(); 82 | return renderer.getRenderOutput(); 83 | } 84 | ``` 85 | 86 | In the code above, we define a function `shallowRender` to return a component's shallow rendering. 87 | 88 | The [first test case](https://github.com/ruanyf/react-testing-demo/blob/master/test/shallow1.test.js) is to test the title of `App`. It needn't interact with DOM and doesn't involve child-components, so is most suitable for use with shadow rendering. 89 | 90 | ```javascript 91 | describe('Shallow Rendering', function () { 92 | it('App\'s title should be Todos', function () { 93 | const app = shallowRender(App); 94 | expect(app.props.children[0].type).to.equal('h1'); 95 | expect(app.props.children[0].props.children).to.equal('Todos'); 96 | }); 97 | }); 98 | ``` 99 | 100 | You may feel `app.props.children[0].props.children` intimidating, but it is not. Each virtual DOM object has a `props.children` property which contains its all children components. `app.props.children[0]` is the `h1` element whose `props.children` is the text of `h1`. 101 | 102 | The [second test case](https://github.com/ruanyf/react-testing-demo/blob/master/test/shallow2.test.js) is to test the initial state of a `TodoItem` is undone. 103 | 104 | At first, we should modify the function `shallowRender` to accept second parameter. 105 | 106 | ```javascript 107 | import TestUtils from 'react-addons-test-utils'; 108 | 109 | function shallowRender(Component, props) { 110 | const renderer = TestUtils.createRenderer(); 111 | renderer.render(); 112 | return renderer.getRenderOutput(); 113 | } 114 | ``` 115 | 116 | The following is the test case. 117 | 118 | ```javascript 119 | import TodoItem from '../app/components/TodoItem'; 120 | 121 | describe('Shallow Rendering', function () { 122 | it('Todo item should not have todo-done class', function () { 123 | const todoItemData = { id: 0, name: 'Todo one', done: false }; 124 | const todoItem = shallowRender(TodoItem, {todo: todoItemData}); 125 | expect(todoItem.props.children[0].props.className.indexOf('todo-done')).to.equal(-1); 126 | }); 127 | }); 128 | ``` 129 | 130 | In the code above, since [`TodoItem`](https://github.com/ruanyf/react-testing-demo/blob/master/app/components/TodoItem.jsx) is a child component of [`App`](https://github.com/ruanyf/react-testing-demo/blob/master/app/components/App.jsx), we have to call `shallowRender` function with `TodoItem`, otherwise it will not be rendered. In our demo, if the state of a `TodoItem` is undone, the `class` property (`props.className`) contains no `todo-done`. 131 | 132 | ### renderIntoDocument 133 | 134 | The second testing choice of official Test Utilities is to render a React component into a real DOM node. `renderIntoDocument` method is used for this purpose. 135 | 136 | ```javascript 137 | import TestUtils from 'react-addons-test-utils'; 138 | import App from '../app/components/App'; 139 | 140 | const app = TestUtils.renderIntoDocument(); 141 | ``` 142 | 143 | `renderIntoDocument` method requires a DOM, otherwise throws an error. Before running the test case, DOM environment (includes `window`, `document` and `navigator` Object) should be available. So we use [jsdom](https://github.com/tmpvar/jsdom) to implement the DOM environment. 144 | 145 | ```javascript 146 | import jsdom from 'jsdom'; 147 | 148 | if (typeof document === 'undefined') { 149 | global.document = jsdom.jsdom(''); 150 | global.window = document.defaultView; 151 | global.navigator = global.window.navigator; 152 | } 153 | ``` 154 | 155 | We save the code above into [`test/setup.js`](https://github.com/ruanyf/react-testing-demo/blob/master/test/setup.js). Then modify `package.json`. 156 | 157 | ```javascript 158 | { 159 | "scripts": { 160 | "test": "mocha --compilers js:babel-core/register --require ./test/setup.js", 161 | }, 162 | } 163 | ``` 164 | 165 | Now every time we run `npm test`, `setup.js` will be required into test script to run together. 166 | 167 | The [third test case](https://github.com/ruanyf/react-testing-demo/blob/master/test/dom1.test.js) is to test the delete button. 168 | 169 | ```javascript 170 | describe('DOM Rendering', function () { 171 | it('Click the delete button, the Todo item should be deleted', function () { 172 | const app = TestUtils.renderIntoDocument(); 173 | let todoItems = TestUtils.scryRenderedDOMComponentsWithTag(app, 'li'); 174 | let todoLength = todoItems.length; 175 | let deleteButton = todoItems[0].querySelector('button'); 176 | TestUtils.Simulate.click(deleteButton); 177 | let todoItemsAfterClick = TestUtils.scryRenderedDOMComponentsWithTag(app, 'li'); 178 | expect(todoItemsAfterClick.length).to.equal(todoLength - 1); 179 | }); 180 | }); 181 | ``` 182 | 183 | In the code above, first, `scryRenderedDOMComponentsWithTag` method finds all `li` elements of the `app` component. Next, get out `todoItems[0]` and find the delete button from it. Then use `TestUtils.Simulate.click` to simulate the click action upon it. Last, expect the new number of all `li` elements to be less one than the old number. 184 | 185 | Test Utilities provides many methods to find DOM elements from a React component. 186 | 187 | > - [scryRenderedDOMComponentsWithClass](https://facebook.github.io/react/docs/test-utils.html#scryrendereddomcomponentswithclass): Finds all instances of components in the rendered tree that are DOM components with the class name matching className. 188 | > - [findRenderedDOMComponentWithClass](https://facebook.github.io/react/docs/test-utils.html#findrendereddomcomponentwithclass): Like scryRenderedDOMComponentsWithClass() but expects there to be one result, and returns that one result, or throws exception if there is any other number of matches besides one. 189 | > - [scryRenderedDOMComponentsWithTag](https://facebook.github.io/react/docs/test-utils.html#scryrendereddomcomponentswithtag): Finds all instances of components in the rendered tree that are DOM components with the tag name matching tagName. 190 | > - [findRenderedDOMComponentWithTag](https://facebook.github.io/react/docs/test-utils.html#findrendereddomcomponentwithtag): Like scryRenderedDOMComponentsWithTag() but expects there to be one result, and returns that one result, or throws exception if there is any other number of matches besides one. 191 | > - [scryRenderedComponentsWithType](https://facebook.github.io/react/docs/test-utils.html#scryrenderedcomponentswithtype): Finds all instances of components with type equal to componentClass. 192 | > - [findRenderedComponentWithType](https://facebook.github.io/react/docs/test-utils.html#findrenderedcomponentwithtype): Same as scryRenderedComponentsWithType() but expects there to be one result and returns that one result, or throws exception if there is any other number of matches besides one. 193 | > - [findAllInRenderedTree](https://facebook.github.io/react/docs/test-utils.html#findallinrenderedtree): Traverse all components in tree and accumulate all components where test(component) is true. 194 | 195 | These methods are hard to spell. Luckily, we have another more concise ways to find DOM nodes from a React component. 196 | 197 | ### findDOMNode 198 | 199 | If a React component has been mounted into the DOM, `react-dom` module's `findDOMNode` method returns the corresponding native browser DOM element. 200 | 201 | We use it to write the [fourth test case](https://github.com/ruanyf/react-testing-demo/blob/master/test/dom2.test.js). It is to test the toggle behavior when a user clicks the Todo item. 202 | 203 | ```javascript 204 | import {findDOMNode} from 'react-dom'; 205 | 206 | describe('DOM Rendering', function (done) { 207 | it('When click the Todo item,it should become done', function () { 208 | const app = TestUtils.renderIntoDocument(); 209 | const appDOM = findDOMNode(app); 210 | const todoItem = appDOM.querySelector('li:first-child span'); 211 | let isDone = todoItem.classList.contains('todo-done'); 212 | TestUtils.Simulate.click(todoItem); 213 | expect(todoItem.classList.contains('todo-done')).to.be.equal(!isDone); 214 | }); 215 | }); 216 | ``` 217 | 218 | In the code above, `findDOMNode` method returns `App`'s DOM node. Then we find out the first `li` element in it, and simulate a click action upon it. Last, we expect the `todo-done` class in `todoItem.classList` to toggle. 219 | 220 | The [fifth test case](https://github.com/ruanyf/react-testing-demo/blob/master/test/dom3.test.js) is to test adding a new Todo item. 221 | 222 | ```javascript 223 | describe('DOM Rendering', function (done) { 224 | it('Add an new Todo item, when click the new todo button', function () { 225 | const app = TestUtils.renderIntoDocument(); 226 | const appDOM = findDOMNode(app); 227 | let todoItemsLength = appDOM.querySelectorAll('.todo-text').length; 228 | let addInput = appDOM.querySelector('input'); 229 | addInput.value = 'Todo four'; 230 | let addButton = appDOM.querySelector('.add-todo button'); 231 | TestUtils.Simulate.click(addButton); 232 | expect(appDOM.querySelectorAll('.todo-text').length).to.be.equal(todoItemsLength + 1); 233 | }); 234 | }); 235 | ``` 236 | 237 | In the code above, at first, we find the `input` box and add a value into it. Then, we find the `Add Todo` button and simulate the click action upon it. Last, we expect the new Todo item to be appended into the Todo list. 238 | 239 | ## Enzyme Library 240 | 241 | [Enzyme](https://github.com/airbnb/enzyme) is a wrapper library of official Test Utilities, mimicking jQuery's API to provide an intuitive and flexible way to test React component. 242 | 243 | It provides three ways to do the testing. 244 | 245 | > - `shallow` 246 | > - `render` 247 | > - `mount` 248 | 249 | ### shallow 250 | 251 | [shallow](https://github.com/airbnb/enzyme/blob/master/docs/api/shallow.md) is a wrapper of Test Utilities' shallow rendering. 252 | 253 | The following is the [first test case](https://github.com/ruanyf/react-testing-demo/blob/master/test/enzyme1.test.js#L6) to test App's title. 254 | 255 | ```javascript 256 | import {shallow} from 'enzyme'; 257 | 258 | describe('Enzyme Shallow', function () { 259 | it('App\'s title should be Todos', function () { 260 | let app = shallow(); 261 | expect(app.find('h1').text()).to.equal('Todos'); 262 | }); 263 | }; 264 | ``` 265 | 266 | In the code above, `shallow` method returns the shallow rendering of `App`, and `app.find` method returns its `h1` element, and `text` method returns the element's text. 267 | 268 | Please keep in mind that `.find` method only supports simple selectors. When meeting complex selectors, it returns no results. 269 | 270 | ```bash 271 | component.find('.my-class'); // by class name 272 | component.find('#my-id'); // by id 273 | component.find('td'); // by tag 274 | component.find('div.custom-class'); // by compound selector 275 | component.find(TableRow); // by constructor 276 | component.find('TableRow'); // by display name 277 | ``` 278 | 279 | ### render 280 | 281 | [`render`](https://github.com/airbnb/enzyme/blob/master/docs/api/render.md) is used to render React components to static HTML and analyze the resulting HTML structure. It returns a wrapper very similar to `shallow`; however, render uses a third party HTML parsing and traversal library Cheerio. This means it returns a CheerioWrapper. 282 | 283 | The following is the [second test case](https://github.com/ruanyf/react-testing-demo/blob/master/test/enzyme1.test.js#L13) to test the initial state of Todo items. 284 | 285 | ```javascript 286 | import {render} from 'enzyme'; 287 | 288 | describe('Enzyme Render', function () { 289 | it('Todo item should not have todo-done class', function () { 290 | let app = render(); 291 | expect(app.find('.todo-done').length).to.equal(0); 292 | }); 293 | }); 294 | ``` 295 | 296 | In the code above, you should see, no matter a ShallowWapper or a CheerioWrapper, Enzyme provides them with the same API (`find` method). 297 | 298 | ### mount 299 | 300 | [`mount`](https://github.com/airbnb/enzyme/blob/master/docs/api/mount.md) is the method to mount your React component into a real DOM node. 301 | 302 | The following is the [third test case](https://github.com/ruanyf/react-testing-demo/blob/master/test/enzyme1.test.js#L21) to test the delete button. 303 | 304 | ```javascript 305 | import {mount} from 'enzyme'; 306 | 307 | describe('Enzyme Mount', function () { 308 | it('Delete Todo', function () { 309 | let app = mount(); 310 | let todoLength = app.find('li').length; 311 | app.find('button.delete').at(0).simulate('click'); 312 | expect(app.find('li').length).to.equal(todoLength - 1); 313 | }); 314 | }); 315 | ``` 316 | 317 | In the code above, `find` method returns an object containing all eligible children components. `at` method returns the child component at the specified position and `simulate` method simulates some action upon it. 318 | 319 | The following is the [fourth test case](https://github.com/ruanyf/react-testing-demo/blob/master/test/enzyme1.test.js#L28) to test the toggle behaviour of a Todo item. 320 | 321 | ```javascript 322 | import {mount} from 'enzyme'; 323 | 324 | describe('Enzyme Mount', function () { 325 | it('Turning a Todo item into Done', function () { 326 | let app = mount(); 327 | let todoItem = app.find('.todo-text').at(0); 328 | todoItem.simulate('click'); 329 | expect(todoItem.hasClass('todo-done')).to.equal(true); 330 | }); 331 | }); 332 | ``` 333 | 334 | The following is the [fifth test case](https://github.com/ruanyf/react-testing-demo/blob/master/test/enzyme1.test.js#L35) to test the `Add Todo` button. 335 | 336 | ```javascript 337 | import {mount} from 'enzyme'; 338 | 339 | describe('Enzyme Mount', function () { 340 | it('Add a new Todo', function () { 341 | let app = mount(); 342 | let todoLength = app.find('li').length; 343 | let addInput = app.find('input').get(0); 344 | addInput.value = 'Todo Four'; 345 | app.find('.add-button').simulate('click'); 346 | expect(app.find('li').length).to.equal(todoLength + 1); 347 | }); 348 | }); 349 | ``` 350 | 351 | ### API List 352 | 353 | The following is an incomplete list of Enzyme API. It should give you a general concept of Enzyme's usage. 354 | 355 | - `.get(index)`: Returns the node at the provided index of the current wrapper 356 | - `.at(index)`: Returns a wrapper of the node at the provided index of the current wrapper 357 | - `.first()`: Returns a wrapper of the first node of the current wrapper 358 | - `.last()`: Returns a wrapper of the last node of the current wrapper 359 | - `.type()`: Returns the type of the current node of the wrapper 360 | - `.text()`: Returns a string representation of the text nodes in the current render tree 361 | - `.html()`: Returns a static HTML rendering of the current node 362 | - `.props()`: Returns the props of the root component 363 | - `.prop(key)`: Returns the named prop of the root component 364 | - `.state([key])`: Returns the state of the root component 365 | - `.setState(nextState)`: Manually sets state of the root component 366 | - `.setProps(nextProps)`: Manually sets props of the root component 367 | 368 | ## Licence 369 | 370 | MIT 371 | -------------------------------------------------------------------------------- /app/components/AddTodo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TodoStore from '../stores/TodoStore'; 3 | 4 | export default class AddTodo extends React.Component { 5 | addTodo() { 6 | const newTodoName = this.refs.todoTitle.value; 7 | if (newTodoName) { 8 | TodoStore.addNewTodo({ 9 | name: newTodoName 10 | }); 11 | TodoStore.emitChange(); 12 | this.refs.todoTitle.value = ''; 13 | } 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 | 20 | 23 |
24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AddTodo from './AddTodo'; 3 | import TodoList from './TodoList'; 4 | 5 | export default class App extends React.Component { 6 | render() { 7 | return ( 8 |
9 |

Todos

10 | 11 | 12 |
13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/components/TodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TodoStore from '../stores/TodoStore'; 3 | 4 | export default class Todo extends React.Component { 5 | toggleDone(e) { 6 | e.preventDefault(); 7 | TodoStore.toggleDone(this.props.todo.id); 8 | TodoStore.emitChange(); 9 | } 10 | 11 | deleteTodo(e) { 12 | e.preventDefault(); 13 | TodoStore.deleteTodo(this.props.todo.id); 14 | TodoStore.emitChange(); 15 | } 16 | 17 | render() { 18 | const todo = this.props.todo; 19 | const todoDone = todo.done ? 'todo-done' : ''; 20 | return ( 21 |
  • 22 | {todo.name} 23 | 24 |
  • 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/components/TodoList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TodoStore from '../stores/TodoStore'; 3 | import TodoItem from './TodoItem'; 4 | 5 | export default class TodoList extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = TodoStore.getAll(); 9 | } 10 | 11 | componentDidMount() { 12 | TodoStore.addChangeListener(this._onChange.bind(this)); 13 | } 14 | 15 | componentWillUnmount() { 16 | TodoStore.removeChangeListener(this._onChange.bind(this)); 17 | } 18 | 19 | _onChange() { 20 | this.setState(TodoStore.getAll()); 21 | } 22 | 23 | render() { 24 | const TodoItemList = this.state.todos.map(todo => { 25 | return ( 26 | 27 | ); 28 | }); 29 | return ( 30 |
      {TodoItemList}
    31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/img/demo-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/react-testing-demo/ad6745163c55d47bb194a7e08975f15627d370af/app/img/demo-delete.png -------------------------------------------------------------------------------- /app/img/demo-newitem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/react-testing-demo/ad6745163c55d47bb194a7e08975f15627d370af/app/img/demo-newitem.png -------------------------------------------------------------------------------- /app/img/demo-toggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/react-testing-demo/ad6745163c55d47bb194a7e08975f15627d370af/app/img/demo-toggle.png -------------------------------------------------------------------------------- /app/img/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/react-testing-demo/ad6745163c55d47bb194a7e08975f15627d370af/app/img/demo.png -------------------------------------------------------------------------------- /app/img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/react-testing-demo/ad6745163c55d47bb194a7e08975f15627d370af/app/img/react.png -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Testing Demo 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/main.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | 5 | li span.todo-done { 6 | text-decoration: line-through; 7 | color: green; 8 | } 9 | 10 | li span.todo-text { 11 | display: inline-block; 12 | width: 6em; 13 | text-overflow: ellipsis; 14 | /* Required for text-overflow to do anything */ 15 | white-space: nowrap; 16 | overflow: hidden; 17 | } 18 | -------------------------------------------------------------------------------- /app/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.body.appendChild(document.createElement('div')) 8 | ); 9 | -------------------------------------------------------------------------------- /app/stores/TodoStore.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import assign from 'object-assign'; 3 | 4 | const TodoStore = assign({}, EventEmitter.prototype, { 5 | items: { 6 | todos: [ 7 | { id: 0, name: 'Todo one', done: false }, 8 | { id: 1, name: 'Todo two', done: false }, 9 | { id: 2, name: 'Todo three', done: false }, 10 | ] 11 | }, 12 | 13 | nextId: 3, 14 | 15 | getAll: function getAll() { 16 | return this.items; 17 | }, 18 | 19 | emitChange: function emitChange() { 20 | this.emit('change'); 21 | }, 22 | 23 | addChangeListener: function addChangeListener(callback) { 24 | this.on('change', callback); 25 | }, 26 | 27 | removeChangeListener: function removeChangeListener(callback) { 28 | this.removeListener('change', callback); 29 | }, 30 | 31 | addNewTodo: function addNewTodo(todo) { 32 | const todos = this.items.todos; 33 | if (!todos || typeof this.items.todos.length !== 'number') { 34 | this.items.todos = []; 35 | } 36 | todo.id = this.nextId++; 37 | todo.done = false; 38 | this.items.todos.push(todo); 39 | }, 40 | 41 | toggleDone: function toggleDone(id) { 42 | this.items.todos = this.items.todos.map(todo => { 43 | if (todo.id === id) { 44 | todo.done = !todo.done; 45 | } 46 | return todo; 47 | }); 48 | }, 49 | 50 | deleteTodo: function deleteTodo(id) { 51 | this.items.todos = this.items.todos.filter((todo) => todo.id !== id); 52 | } 53 | }); 54 | 55 | export default TodoStore; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-testing-demo", 3 | "version": "1.0.0", 4 | "description": "A tutorial of testing React components", 5 | "main": "app/main.jsx", 6 | "scripts": { 7 | "lint": "eslint 'app/**/*.@(js|jsx)'", 8 | "test": "mocha --compilers js:babel-core/register --require ./test/setup.js", 9 | "build": "webpack", 10 | "start": "webpack-dev-server --devtool eval --progress --hot --colors --content-base app", 11 | "deploy": "NODE_ENV=production webpack -p --config webpack.production.config.js", 12 | "validate": "npm ls" 13 | }, 14 | "dependencies": { 15 | "babel-runtime": "~6.2.0", 16 | "react": "~0.14.3", 17 | "react-dom": "~0.14.3" 18 | }, 19 | "devDependencies": { 20 | "babel-core": "~6.2.1", 21 | "babel-eslint": "~4.1.6", 22 | "babel-loader": "~6.2.0", 23 | "babel-plugin-transform-runtime": "~6.1.18", 24 | "babel-preset-es2015": "~6.1.18", 25 | "babel-preset-react": "~6.1.18", 26 | "babel-preset-stage-0": "~6.1.18", 27 | "chai": "^3.4.1", 28 | "copy-webpack-plugin": "~0.3.3", 29 | "css-loader": "~0.23.0", 30 | "enzyme": "^1.4.1", 31 | "eslint": "~1.10.1", 32 | "eslint-config-airbnb": "~1.0.0", 33 | "eslint-plugin-react": "~3.10.0", 34 | "jsdom": "^7.2.2", 35 | "jsx-test": "~2.1.0", 36 | "mocha": "^2.3.4", 37 | "object-assign": "^4.0.1", 38 | "open-browser-webpack-plugin": "0.0.1", 39 | "precommit-hook": "~3.0.0", 40 | "react-addons-test-utils": "^0.14.6", 41 | "style-loader": "~0.13.0", 42 | "webpack": "~1.12.9", 43 | "webpack-dev-server": "~1.14.0" 44 | }, 45 | "keywords": [ 46 | "react", 47 | "test", 48 | "enzyme" 49 | ], 50 | "author": "Ruan Yifeng", 51 | "license": "MIT", 52 | "pre-commit": [ 53 | "lint" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /test/dom1.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | import App from '../app/components/App'; 5 | 6 | describe('DOM Rendering', function () { 7 | it('Click the delete button, the Todo item should be deleted', function () { 8 | const app = TestUtils.renderIntoDocument(); 9 | let todoItems = TestUtils.scryRenderedDOMComponentsWithTag(app, 'li'); 10 | let todoLength = todoItems.length; 11 | let deleteButton = todoItems[0].querySelector('button'); 12 | TestUtils.Simulate.click(deleteButton); 13 | let todoItemsAfterClick = TestUtils.scryRenderedDOMComponentsWithTag(app, 'li'); 14 | expect(todoItemsAfterClick.length).to.equal(todoLength - 1); 15 | }); 16 | }); 17 | 18 | -------------------------------------------------------------------------------- /test/dom2.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {findDOMNode} from 'react-dom'; 3 | import jsdom from 'jsdom'; 4 | import TestUtils from 'react-addons-test-utils'; 5 | import {expect} from 'chai'; 6 | 7 | import App from '../app/components/App'; 8 | 9 | describe('DOM Rendering', function (done) { 10 | it('When click the Todo item,it should become done', function () { 11 | const app = TestUtils.renderIntoDocument(); 12 | const appDOM = findDOMNode(app); 13 | const todoItem = appDOM.querySelector('li:first-child span'); 14 | let isDone = todoItem.classList.contains('todo-done'); 15 | TestUtils.Simulate.click(todoItem); 16 | expect(todoItem.classList.contains('todo-done')).to.be.equal(!isDone); 17 | // make the item returns to previous state 18 | TestUtils.Simulate.click(todoItem); 19 | }); 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /test/dom3.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {findDOMNode} from 'react-dom'; 3 | import jsdom from 'jsdom'; 4 | import {expect} from 'chai'; 5 | import TestUtils from 'react-addons-test-utils'; 6 | import App from '../app/components/App'; 7 | 8 | describe('DOM Rendering', function (done) { 9 | it('Add an new Todo item, when click the new todo button', function () { 10 | const app = TestUtils.renderIntoDocument(); 11 | const appDOM = findDOMNode(app); 12 | let todoItemsLength = appDOM.querySelectorAll('.todo-text').length; 13 | let addInput = appDOM.querySelector('input'); 14 | addInput.value = 'Todo four'; 15 | let addButton = appDOM.querySelector('.add-todo button'); 16 | TestUtils.Simulate.click(addButton); 17 | expect(appDOM.querySelectorAll('.todo-text').length).to.be.equal(todoItemsLength + 1); 18 | }); 19 | }); 20 | 21 | -------------------------------------------------------------------------------- /test/enzyme1.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {shallow, mount, render} from 'enzyme'; 3 | import {expect} from 'chai'; 4 | import App from '../app/components/App'; 5 | 6 | describe('Enzyme Shallow', function () { 7 | it('App\'s title should be Todos', function () { 8 | let app = shallow(); 9 | expect(app.find('h1').text()).to.equal('Todos'); 10 | }); 11 | }); 12 | 13 | describe('Enzyme Render', function () { 14 | it('Todo item should not have todo-done class', function () { 15 | let app = render(); 16 | expect(app.find('.todo-done').length).to.equal(0); 17 | }); 18 | }); 19 | 20 | describe('Enzyme Mount', function () { 21 | it('Delete Todo', function () { 22 | let app = mount(); 23 | let todoLength = app.find('li').length; 24 | app.find('button.delete').at(0).simulate('click'); 25 | expect(app.find('li').length).to.equal(todoLength - 1); 26 | }); 27 | 28 | it('Turning a Todo item into Done', function () { 29 | let app = mount(); 30 | let todoItem = app.find('.todo-text').at(0); 31 | todoItem.simulate('click'); 32 | expect(todoItem.hasClass('todo-done')).to.equal(true); 33 | }); 34 | 35 | it('Add a new Todo', function () { 36 | let app = mount(); 37 | let todoLength = app.find('li').length; 38 | let addInput = app.find('input').get(0); 39 | addInput.value = 'Todo Four'; 40 | app.find('.add-button').simulate('click'); 41 | expect(app.find('li').length).to.equal(todoLength + 1); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom'; 2 | 3 | if (typeof document === 'undefined') { 4 | global.document = jsdom.jsdom(''); 5 | global.window = document.defaultView; 6 | global.navigator = global.window.navigator; 7 | } 8 | -------------------------------------------------------------------------------- /test/shallow1.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-addons-test-utils'; 3 | import { expect } from 'chai'; 4 | import App from '../app/components/App'; 5 | 6 | function shallowRender(Component) { 7 | const renderer = TestUtils.createRenderer(); 8 | renderer.render(); 9 | return renderer.getRenderOutput(); 10 | } 11 | 12 | describe('Shallow Rendering', function () { 13 | it('App\'s title should be Todos', function () { 14 | const app = shallowRender(App); 15 | // component's shallow rendering has props.children 16 | expect(app.props.children[0].type).to.equal('h1'); 17 | expect(app.props.children[0].props.children).to.equal('Todos'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/shallow2.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-addons-test-utils'; 3 | import {expect} from 'chai'; 4 | import TodoItem from '../app/components/TodoItem'; 5 | 6 | function shallowRender(Component, props) { 7 | const renderer = TestUtils.createRenderer(); 8 | renderer.render(); 9 | return renderer.getRenderOutput(); 10 | } 11 | 12 | describe('Shallow Rendering', function () { 13 | it('Todo item should not have todo-done class', function () { 14 | const todoItemData = { id: 0, name: 'Todo one', done: false }; 15 | const todoItem = shallowRender(TodoItem, {todo: todoItemData}); 16 | expect(todoItem.props.children[0].props.className.indexOf('todo-done')).to.equal(-1); 17 | }); 18 | }); 19 | 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var OpenBrowserPlugin = require('open-browser-webpack-plugin'); 4 | 5 | module.exports = { 6 | devServer: { 7 | historyApiFallback: true, 8 | hot: true, 9 | inline: true, 10 | progress: true, 11 | contentBase: './app', 12 | port: 8080 13 | }, 14 | entry: [ 15 | 'webpack/hot/dev-server', 16 | 'webpack-dev-server/client?http://localhost:8080', 17 | path.resolve(__dirname, 'app/main.jsx') 18 | ], 19 | output: { 20 | path: __dirname + '/build', 21 | publicPath: '/', 22 | filename: './bundle.js' 23 | }, 24 | resolve: { 25 | extensions: ['', '.js', '.jsx'] 26 | }, 27 | module: { 28 | loaders:[ 29 | { test: /\.css$/, include: path.resolve(__dirname, 'app'), loader: 'style-loader!css-loader' }, 30 | { test: /\.js[x]?$/, include: path.resolve(__dirname, 'app'), exclude: /node_modules/, loader: 'babel-loader' }, 31 | ] 32 | }, 33 | plugins: [ 34 | new webpack.HotModuleReplacementPlugin(), 35 | new OpenBrowserPlugin({ url: 'http://localhost:8080' }) 36 | ] 37 | }; 38 | -------------------------------------------------------------------------------- /webpack.production.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var uglifyJsPlugin = webpack.optimize.UglifyJsPlugin; 4 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | 6 | module.exports = { 7 | devtool: 'cheap-source-map', 8 | entry: [ 9 | path.resolve(__dirname, 'app/main.jsx'), 10 | ], 11 | output: { 12 | path: __dirname + '/build', 13 | publicPath: '/', 14 | filename: './bundle.js' 15 | }, 16 | module: { 17 | loaders:[ 18 | { test: /\.css$/, include: path.resolve(__dirname, 'app'), loader: 'style-loader!css-loader' }, 19 | { test: /\.js[x]?$/, include: path.resolve(__dirname, 'app'), exclude: /node_modules/, loader: 'babel-loader' }, 20 | ] 21 | }, 22 | resolve: { 23 | extensions: ['', '.js', '.jsx'] 24 | }, 25 | plugins: [ 26 | new webpack.optimize.DedupePlugin(), 27 | new uglifyJsPlugin({ 28 | compress: { 29 | warnings: false 30 | } 31 | }), 32 | new CopyWebpackPlugin([ 33 | { from: './app/index.html', to: 'index.html' }, 34 | { from: './app/main.css', to: 'main.css' } 35 | ]), 36 | ] 37 | }; 38 | --------------------------------------------------------------------------------