• Stars
    star
    214
  • Rank 184,678 (Top 4 %)
  • Language
    JavaScript
  • Created almost 9 years ago
  • Updated over 6 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A tutorial of testing React components

This repo shows you how to test React component. It is loosely based on Jack Franklin's article "Testing React Applications".

Demo

$ git clone https://github.com/ruanyf/react-testing-demo.git
$ cd react-testing-demo && npm install
$ npm start
$ open http://127.0.0.1:8080

Now, you visit http://127.0.0.1:8080/, and should see a Todo app.

There are 5 places to test.

  1. App's title should be "Todos"
  2. Initial state of a Todo item should be right ("done" or "undone")
  3. Click a Todo item, its state should be toggled (from "undone" to "done", or vice versa)
  4. Click a Delete button, the Todo item should be deleted
  5. Click the Add Todo button, a new Todo item should be added into the TodoList

All test cases have been written. You run npm test to find the test result.

$ npm test

Index

Testing Library

The most important tool of testing React is official Test Utilities, but it only provides low-level API. As a result, some third-party test libraries are built based on it. Airbnb's Enzyme library is the easiest one to use among them.

Thus every test case has at least two ways to write.

  • Test Utilities' way
  • Enzyme's way

This repo will show you both of them.

React official Test Utilities

Since a component could be rendered into either a virtual DOM object (React.Component's instance) or a real DOM node, Test Utilities library gives you two testing choices.

  • Shallow Rendering: testing a virtual DOM object
  • DOM Rendering: testing a real DOM node

Shallow Rendering

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.

At first, import the Test Utilities in your test case script.

import TestUtils from 'react-addons-test-utils';

Then, write a Shallow Rendering function.

import TestUtils from 'react-addons-test-utils';

function shallowRender(Component) {
  const renderer = TestUtils.createRenderer();
  renderer.render(<Component/>);
  return renderer.getRenderOutput();
}

In the code above, we define a function shallowRender to return a component's shallow rendering.

The first test case 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.

describe('Shallow Rendering', function () {
  it('App\'s title should be Todos', function () {
    const app = shallowRender(App);
    expect(app.props.children[0].type).to.equal('h1');
    expect(app.props.children[0].props.children).to.equal('Todos');
  });
});

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.

The second test case is to test the initial state of a TodoItem is undone.

At first, we should modify the function shallowRender to accept second parameter.

import TestUtils from 'react-addons-test-utils';

function shallowRender(Component, props) {
  const renderer = TestUtils.createRenderer();
  renderer.render(<Component {...props}/>);
  return renderer.getRenderOutput();
}

The following is the test case.

import TodoItem from '../app/components/TodoItem';

describe('Shallow Rendering', function () {
  it('Todo item should not have todo-done class', function () {
    const todoItemData = { id: 0, name: 'Todo one', done: false };
    const todoItem = shallowRender(TodoItem, {todo: todoItemData});
    expect(todoItem.props.children[0].props.className.indexOf('todo-done')).to.equal(-1);
  });
});

In the code above, since TodoItem is a child component of App, 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.

renderIntoDocument

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.

import TestUtils from 'react-addons-test-utils';
import App from '../app/components/App';

const app = TestUtils.renderIntoDocument(<App/>);

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 to implement the DOM environment.

import jsdom from 'jsdom';

if (typeof document === 'undefined') {
  global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
  global.window = document.defaultView;
  global.navigator = global.window.navigator;
}

We save the code above into test/setup.js. Then modify package.json.

{
  "scripts": {
    "test": "mocha --compilers js:babel-core/register --require ./test/setup.js",
  },
}

Now every time we run npm test, setup.js will be required into test script to run together.

The third test case is to test the delete button.

describe('DOM Rendering', function () {
  it('Click the delete button, the Todo item should be deleted', function () {
    const app = TestUtils.renderIntoDocument(<App/>);
    let todoItems = TestUtils.scryRenderedDOMComponentsWithTag(app, 'li');
    let todoLength = todoItems.length;
    let deleteButton = todoItems[0].querySelector('button');
    TestUtils.Simulate.click(deleteButton);
    let todoItemsAfterClick = TestUtils.scryRenderedDOMComponentsWithTag(app, 'li');
    expect(todoItemsAfterClick.length).to.equal(todoLength - 1);
  });
});

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.

Test Utilities provides many methods to find DOM elements from a React component.

  • scryRenderedDOMComponentsWithClass: Finds all instances of components in the rendered tree that are DOM components with the class name matching className.
  • 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.
  • scryRenderedDOMComponentsWithTag: Finds all instances of components in the rendered tree that are DOM components with the tag name matching tagName.
  • 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.
  • scryRenderedComponentsWithType: Finds all instances of components with type equal to componentClass.
  • 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.
  • findAllInRenderedTree: Traverse all components in tree and accumulate all components where test(component) is true.

These methods are hard to spell. Luckily, we have another more concise ways to find DOM nodes from a React component.

findDOMNode

If a React component has been mounted into the DOM, react-dom module's findDOMNode method returns the corresponding native browser DOM element.

We use it to write the fourth test case. It is to test the toggle behavior when a user clicks the Todo item.

import {findDOMNode} from 'react-dom';

describe('DOM Rendering', function (done) {
  it('When click the Todo item,it should become done', function () {
    const app = TestUtils.renderIntoDocument(<App/>);
    const appDOM = findDOMNode(app);
    const todoItem = appDOM.querySelector('li:first-child span');
    let isDone = todoItem.classList.contains('todo-done');
    TestUtils.Simulate.click(todoItem);
    expect(todoItem.classList.contains('todo-done')).to.be.equal(!isDone);
  });
});

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.

The fifth test case is to test adding a new Todo item.

describe('DOM Rendering', function (done) {
  it('Add an new Todo item, when click the new todo button', function () {
    const app = TestUtils.renderIntoDocument(<App/>);
    const appDOM = findDOMNode(app);
    let todoItemsLength = appDOM.querySelectorAll('.todo-text').length;
    let addInput = appDOM.querySelector('input');
    addInput.value = 'Todo four';
    let addButton = appDOM.querySelector('.add-todo button');
    TestUtils.Simulate.click(addButton);
    expect(appDOM.querySelectorAll('.todo-text').length).to.be.equal(todoItemsLength + 1);
  });
});

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.

Enzyme Library

Enzyme is a wrapper library of official Test Utilities, mimicking jQuery's API to provide an intuitive and flexible way to test React component.

It provides three ways to do the testing.

  • shallow
  • render
  • mount

shallow

shallow is a wrapper of Test Utilities' shallow rendering.

The following is the first test case to test App's title.

import {shallow} from 'enzyme';

describe('Enzyme Shallow', function () {
  it('App\'s title should be Todos', function () {
    let app = shallow(<App/>);
    expect(app.find('h1').text()).to.equal('Todos');
  });
};

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.

Please keep in mind that .find method only supports simple selectors. When meeting complex selectors, it returns no results.

component.find('.my-class'); // by class name
component.find('#my-id'); // by id
component.find('td'); // by tag
component.find('div.custom-class'); // by compound selector
component.find(TableRow); // by constructor
component.find('TableRow'); // by display name

render

render 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.

The following is the second test case to test the initial state of Todo items.

import {render} from 'enzyme';

describe('Enzyme Render', function () {
  it('Todo item should not have todo-done class', function () {
    let app = render(<App/>);
    expect(app.find('.todo-done').length).to.equal(0);
  });
});

In the code above, you should see, no matter a ShallowWapper or a CheerioWrapper, Enzyme provides them with the same API (find method).

mount

mount is the method to mount your React component into a real DOM node.

The following is the third test case to test the delete button.

import {mount} from 'enzyme';

describe('Enzyme Mount', function () {
  it('Delete Todo', function () {
    let app = mount(<App/>);
    let todoLength = app.find('li').length;
    app.find('button.delete').at(0).simulate('click');
    expect(app.find('li').length).to.equal(todoLength - 1);
  });
});

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.

The following is the fourth test case to test the toggle behaviour of a Todo item.

import {mount} from 'enzyme';

describe('Enzyme Mount', function () {
  it('Turning a Todo item into Done', function () {
    let app = mount(<App/>);
    let todoItem = app.find('.todo-text').at(0);
    todoItem.simulate('click');
    expect(todoItem.hasClass('todo-done')).to.equal(true);
  });
});

The following is the fifth test case to test the Add Todo button.

import {mount} from 'enzyme';

describe('Enzyme Mount', function () {
  it('Add a new Todo', function () {
    let app = mount(<App/>);
    let todoLength = app.find('li').length;
    let addInput = app.find('input').get(0);
    addInput.value = 'Todo Four';
    app.find('.add-button').simulate('click');
    expect(app.find('li').length).to.equal(todoLength + 1);
  });
});

API List

The following is an incomplete list of Enzyme API. It should give you a general concept of Enzyme's usage.

  • .get(index): Returns the node at the provided index of the current wrapper
  • .at(index): Returns a wrapper of the node at the provided index of the current wrapper
  • .first(): Returns a wrapper of the first node of the current wrapper
  • .last(): Returns a wrapper of the last node of the current wrapper
  • .type(): Returns the type of the current node of the wrapper
  • .text(): Returns a string representation of the text nodes in the current render tree
  • .html(): Returns a static HTML rendering of the current node
  • .props(): Returns the props of the root component
  • .prop(key): Returns the named prop of the root component
  • .state([key]): Returns the state of the root component
  • .setState(nextState): Manually sets state of the root component
  • .setProps(nextProps): Manually sets props of the root component

Licence

MIT

More Repositories

1

weekly

科技爱好者周刊,每周五发布
33,058
star
2

es6tutorial

《ECMAScript 6入门》是一本开源的 JavaScript 语言教程,全面介绍 ECMAScript 6 新增的语法特性。
JavaScript
20,881
star
3

jstraining

全栈工程师培训材料
18,959
star
4

react-demos

a collection of simple demos of React.js
JavaScript
16,171
star
5

free-books

互联网上的免费书籍
13,692
star
6

document-style-guide

中文技术文档的写作规范
10,968
star
7

webpack-demos

a collection of simple demos of Webpack
JavaScript
9,588
star
8

jstutorial

Javascript tutorial book
CSS
5,421
star
9

simple-bash-scripts

A collection of simple Bash scripts
Shell
1,415
star
10

reading-list

Some books I read
1,309
star
11

react-babel-webpack-boilerplate

a boilerplate for React-Babel-Webpack project
JavaScript
1,154
star
12

articles

personal articles
921
star
13

loppo

an extremely easy static site generator of markdown documents
JavaScript
707
star
14

wechat-miniprogram-demos

微信小程序教程库
599
star
15

book-computer-networks

Free E-Book: Computer Networks - A Systems Approach
596
star
16

koa-demos

A collection of simple demos of Koa
485
star
17

extremely-simple-flux-demo

Learn Flux from an extremely simple demo
JavaScript
442
star
18

css-modules-demos

a collection of simple demos of CSS Modules
JavaScript
395
star
19

fortunes

A collection of fortune database files for Chinese users.
335
star
20

survivor

博客文集《未来世界的幸存者》
CSS
325
star
21

chrome-extension-demo

how to create a Chrome extension
JavaScript
302
star
22

node-oauth-demo

A very simple demo of OAuth2.0 using node.js
JavaScript
301
star
23

mocha-demos

a collection of simple demos of Mocha
JavaScript
254
star
24

tiny-browser-require

A tiny, simple CommonJS require() implemetation in browser-side
JavaScript
237
star
25

road

博客文集《前方的路》
CSS
150
star
26

sina-news

新浪全球实时新闻
JavaScript
133
star
27

github-actions-demo

a demo of GitHub actions for a simple React App
JavaScript
132
star
28

weather-action

An example of GitHub Actions
Shell
107
star
29

user-tracking-demos

demos of tracking users with JavaScript
JavaScript
90
star
30

travis-ci-demo

A beginner tutorial of Travis CI for Node projects
75
star
31

openrecord-demos

an ORM tutorial for nodejs
73
star
32

website

HTML
63
star
33

markdown-it-image-lazy-loading

a markdown-it plugin supporting Chrome 75's native image lazy-loading
JavaScript
54
star
34

flux-todomvc-demo

A simplified version of Flux's official TodoMVC demo
CSS
49
star
35

Google-Calendar-Lite

A single-page webapp of Google Calendar, based on its API.
CSS
41
star
36

nilka

a command-line utility to resize images in batches
JavaScript
39
star
37

webpack-static-site-demo

a demo of generating a static site with React, React-Router, and Webpack
JavaScript
32
star
38

turpan

a wrapped markdown renderer based on markdown-it
JavaScript
27
star
39

hn

A personalized Hacker News
JavaScript
26
star
40

rpio-led-demo

controlling an LED with Raspberry Pi's GPIO
JavaScript
25
star
41

koa-simple-server

A simple koa server demo of logging HTTP request Headers and body
JavaScript
25
star
42

jekyll_demo

A very simple demo of Jekyll
22
star
43

node-systemd-demo

run a Node app as a daemon with Systemd
JavaScript
20
star
44

lvv2-feed

Lvv2.com's RSS feed
JavaScript
16
star
45

blog-stylesheet

my blog's stylesheet
CSS
14
star
46

loppo-theme-oceandeep

the default theme of Loppo
JavaScript
13
star
47

tarim

a template engine, using Lodash's template syntax and supporting including other templates
JavaScript
13
star
48

loppo-theme-ryf

个人网站的 Loppo 主题
CSS
11
star
49

Formula-Online-Generator

using Google Chart api to generate mathematical formulas in a webpage
10
star
50

turpan-remove-space

remove the space between English word and Chinese characters in markdown files
JavaScript
9
star
51

eslint-plugin-ignoreuglify

exclude uglified files from ESLint's linting
JavaScript
3
star
52

slides

JavaScript
3
star