├── .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 | 
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 | 
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 |