附录三、React 测试入门教学

React 测试入门教学

前言

测试是软件开发中非常重要的一个环节,本章我们将带领大家从撰写最简单的测试程式码到整合 Mocha + Chai 官方提供的测试工具和 Airbnb 所设计的 Enzyme 进行 React 测试。

Mocha 测试初体验

Mocha 是目前颇为流行的 JavaScript 测试框架之一,其可以很方便使用于浏览器端和 Node 环境。

Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases.

除了 Mocha 外,尚有许多 JavaScript 单元测试工具可以选择,例如:JasmineKarma 等。但本章我们主要使用 Mocha + Chai 结合 React 官方测试工具和 Enzyme 进行讲解。

在这边我们先介绍一些比较常用的 Mocha 使用方法,让大家熟悉测试的用法(若是已经熟悉撰写测试程式码的读者这部份可以跳过):

  1. 安装环境与套件

    安装 reactreact-dom

    1. $ npm install --save react react-dom

    可以在全域安装 mocha:

    1. $ npm install --global mocha

    也可以在开发环境下本地端安装(同时安装了 babel、eslint、webpack 等相关套件,其中以 mocha、chai、babel 为主要必须):

    1. $ npm install --save-dev babel-core babel-loader babel-eslint babel-preset-react babel-preset-es2015 eslint eslint-config-airbnb eslint-loader eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react webpack webpack-dev-server html-webpack-plugin chai mocha
  2. 测试程式码

    1. describe(test suite):表示一组相关的测试。describe 为一个函数,第一个参数为 test suite的名称,第二个参数为实际执行的函数。
    2. it(test case):表示一个单独测试,为测试里最小单位。it 为一个函数,第一个参数为 test case 的描述名称,第二个参数为实际执行的函数。

      在测试程式码中会包含一个或多个 test suite,而每个 test suite 则会包含一个或多个 test case

  3. 整合 assertion 函式库 Chai

    所谓的 assertion(断言),就是判断程式码的执行成果是否和预期一样,若是不一致则会发生错误。通常一个 test case 会拥有一个或多个 assertion。由于 Mocha 本身是一个测试框架,但不包含 assertion,所以我们使用 Chai 这个适用于浏览器端和 Node 端的 BDD / TDD assertion library。在 Chai 中共提供三种操作 assertion 介面风格:Expect、Assert、Should,在这边我们选择使用比较接近自然语言的 Expect。

    基本上,expect assertion 的写法都是类似:开头为 expect 方法 + toto.be + 结尾 assertion 方法(例如:equal、a/an、ok、match)

  4. Mocha 基本用法

    mocha 若没指定要执行哪个档案,预设会执行 test 资料夹下第一层的测试程式码。若要让 test 资料夹中的子资料夹测试码也执行则要加上 --recursive 参数。

    包含子资料夹:

    1. $ mocha --recursive

    指定一个档案

    1. $ mocha file1.js

    也可以指定多个档案

    1. $ mocha file1.js file2.js

    现在,我们来撰写一个简单的测试程式,亲身感受一下测试的感觉。以下是 react-mocha-test-example/src/modules/add.js,一个加法的函数:

    1. const add = (x, y) => (
    2. x + y
    3. );
    4. export default add;

    接着我们撰写测试这个函数的程式码,测试是否正确。以下是 react-mocha-test-example/src/test/add.test.js

    1. // test add.js
    2. import add from '../src/modules/add';
    3. import { expect } from 'chai';
    4. // describe is test suite, it is test case
    5. describe('test add function', () => (
    6. it('1 + 1 = 2', () => (
    7. expect(add(1, 1)).to.be.equal(2)
    8. ))
    9. ));

    在开始执行 mocha 后由于我们使用了,ES6 的语法所以必须使用 bable 进行转译,否则会出现类似以下的错误:

    1. import add from '../src/modules/add';
    2. ^^^^^^

    我们先行设定 .bablerc,我们在之前已经有安装 babel 相关套件和 presets 所以就会将 ES2015 语法转译。

    1. {
    2. "presets": [
    3. "es2015",
    4. "react",
    5. ],
    6. "plugins": []
    7. }

    此时,我们更改 package.json 中的 scripts,这样方便每次测试执行:

    若是使用本地端:

    1. $ ./node_modules/mocha/bin/mocha --compilers js:babel-core/register

    若是使用全域:

    1. $ mocha --compilers js:babel-core/register

    若是一切顺利,我们就可以看到执行测试成功的结果:

    ```
    $ mocha add.test.js

    test add function

    1. 1 + 1 = 2
  1. 1 passing (181ms)
  2. ```
  1. Mocha 指令参数

    在 Mocha 中有许多可以使用的好用参数,例如:--recursive 可以执行执行测试资料夹下的子资料夹程式码、--reporter 格式 更改测试报告格式(预设是 spec,也可以更改为 tap)、--watch 用来监控测试程式码,当有测试程式码更新就会重新执行、--grep 撷取符合条件的 test case。

    以上这些参数我们可以都整理在 test 资料夹下的 mocha.opts 档案中当作设定资料,此时再次执行 npm run test 就会把参数也使用进去。

    1. --watch
    2. --reporter spec
  2. 非同步测试

    在上面我们讨论的主要是同步的状况,但实际上在开发应用时往往会遇到非同步的情形。而在 Mocha 中每个 test case 最多允许执行 2000 毫秒,当时间超过就会显示错误。为了解决这个问题我们可以在 package.json 中更改:"test": "mocha -t 5000 --compilers js:babel-core/register" 档案。

    为了模拟测试非同步的情境,所以我们必须先安装 axios

    1. $ npm install --save axios

    以下是 react-mocha-test-example/src/test/async.test.js

    1. import axios from 'axios';
    2. import { expect } from 'chai';
    3. it('asynchronous return an object', function(done){
    4. axios
    5. .get('https://api.github.com/users/torvus')
    6. .then(function (response) {
    7. expect(response).to.be.an('object');
    8. done();
    9. })
    10. .catch(function (error) {
    11. console.log(error);
    12. });
    13. });

    由于测试环境是在 Node 中,所以我们必须先安装 node-fetch 来展现 promise 的情境。

    1. $ npm install --save node-fetch

    以下是 react-mocha-test-example/src/test/promise.test.js

    1. import fetch from 'node-fetch';
    2. import { expect } from 'chai';
    3. it('asynchronous fetch promise', function() {
    4. return fetch('https://api.github.com/users/torvus')
    5. .then(function(response) { return response.json() })
    6. .then(function(json) {
    7. expect(json).to.be.an('object');
    8. });
    9. });
  3. 测试使用的 hook

    在 Mocha 中的 test suite 中,有 before()、after()、beforeEach() 和 afterEach() 四种 hook,可以让你设计在特定时间点执行测试。

    1. describe('hooks', function() {
    2. before(function() {
    3. // 在 before 中的 test case 会在所有 test cases 前执行
    4. });
    5. after(function() {
    6. // 在 after 中的 test case 会在所有 test cases 后执行
    7. });
    8. beforeEach(function() {
    9. // 在 beforeEach 中的 test case 会在每个 test cases 前执行
    10. });
    11. afterEach(function() {
    12. // 在 afterEach 中的 test case 会在每个 test cases 后执行
    13. });
    14. // test cases
    15. });

动手实作

在上面我们已经先讲解了 Mocha + Chai 测试工具和基础的测试写法。现在接着我们要来探讨 React 中的测试用法。然而,要在 React 中测试 Component 以及 JSX 语法时,使用传统的测试工具并不方便,所以要整合 Mocha + Chai 官方提供的测试工具和 Airbnb 所设计的 Enzyme(由于官方的测试工具使用起来不太方便所以有第三方针对其进行封装)进行测试。

使用官方测试工具

我们知道在 React 一个重要的特色为 Virtual DOM 所以在官方的测试工具中有提供测试 Virtual DOM 的方法:Shallow Rendering(createRenderer),以及测试真实 DOM 的方法:DOM Rendering(renderIntoDocument)。

  1. Shallow Rendering(createRenderer)

    Shallow Rendering 系指将一个 Virtual DOM 渲染成子 Component,但是只渲染第一层,不渲染所有子组件,因此处理速度快且不需要 DOM 环境。Shallow rendering 在单元测试非常有用,由于只测试一个特定的 component,而重要的不是它的 children。这也意味着改变一个 child component 不会影响 parent component 的测试。

    以下是 react-addons-test-utils-example/src/test/shallowRender.test.js

    1. import React from 'react';
    2. import TestUtils from 'react-addons-test-utils';
    3. import { expect } from 'chai';
    4. import Main from '../src/components/Main';
    5. function shallowRender(Component) {
    6. const renderer = TestUtils.createRenderer();
    7. renderer.render(<Component/>);
    8. return renderer.getRenderOutput();
    9. }
    10. describe('Shallow Rendering', function () {
    11. it('Main title should be h1', function () {
    12. const todoItem = shallowRender(Main);
    13. expect(todoItem.props.children[0].type).to.equal('h1');
    14. expect(todoItem.props.children[0].props.children).to.equal('Todos');
    15. });
    16. });

    以下是 react-addons-test-utils-example/src/test/shallowRenderProps.test.js

    1. import React from 'react';
    2. import TestUtils from 'react-addons-test-utils';
    3. import { expect } from 'chai';
    4. import TodoList from '../src/components/TodoList';
    5. const shallowRender = (Component, props) => {
    6. const renderer = TestUtils.createRenderer();
    7. renderer.render(<Component {...props}/>);
    8. return renderer.getRenderOutput();
    9. }
    10. describe('Shallow Props Rendering', () => {
    11. it('TodoList props check', () => {
    12. const todos = [{ id: 0, text: 'reading'}, { id: 1, text: 'coding'}];
    13. const todoList = shallowRender(TodoList, {todos: todos});
    14. expect(todoList.props.children.type).to.equal('ul');
    15. expect(todoList.props.children.props.children[0].props.children).to.equal('reading');
    16. expect(todoList.props.children.props.children[1].props.children).to.equal('coding');
    17. });
    18. });
  2. DOM Rendering(renderIntoDocument)

    注意,因为 Mocha 运行在 Node 环境中,所以你不会存取到 DOM。所以我们要使用 JSDOM 来模拟真实 DOM 环境。同时我在这边引入 react-dom,这样我们就可以使用 findDOMNode 来选取元素。事实上,findDOMNode 方法的最大优势是提供比 TestUtils 更好的 CSS 选择器,方便开发者选择元素。

    以下是 react-addons-test-utils-example/src/test/setup.test.js

    1. import jsdom from 'jsdom';
    2. if (typeof document === 'undefined') {
    3. global.document = jsdom.jsdom('<!doctype html><html><head></head><body></body></html>');
    4. global.window = document.defaultView;
    5. global.navigator = global.window.navigator;
    6. }

    以下是 react-addons-test-utils-example/src/components/TodoHeader/TodoHeader.js

    1. import React from 'react';
    2. class TodoHeader extends React.Component {
    3. constructor(props) {
    4. super(props);
    5. this.toggleButton = this.toggleButton.bind(this);
    6. this.state = {
    7. isActivated: false,
    8. };
    9. }
    10. toggleButton() {
    11. this.setState({
    12. isActivated: !this.state.isActivated,
    13. })
    14. }
    15. render() {
    16. return (
    17. <div>
    18. <button disabled={this.state.isActivated} onClick={this.toggleButton}>Add</button>
    19. </div>
    20. );
    21. };
    22. }
    23. export default TodoHeader;

    需要留意的是若是 stateless components 使用 TestUtils.renderIntoDocument,要将 renderIntoDocument 包在 <div></div> 内,使用 findDOMNode(TodoHeaderApp).children[0] 取得,不然会回传 null。更进一步细节可以参考这里。不过由于我们是使用 class-based Component 所以不会遇到这个问题。

    以下是 react-addons-test-utils-example/src/test/renderIntoDocument.test.js

    1. import React from 'react';
    2. import TestUtils from 'react-addons-test-utils';
    3. import { expect } from 'chai';
    4. import { findDOMNode } from 'react-dom';
    5. import TodoHeader from '../src/components/TodoHeader';
    6. describe('Simulate Event', function () {
    7. it('When click the button, it will be toggle', function () {
    8. const TodoHeaderApp = TestUtils.renderIntoDocument(<TodoHeader />);
    9. const TodoHeaderDOM = findDOMNode(TodoHeaderApp);
    10. const button = TodoHeaderDOM.querySelector('button');
    11. TestUtils.Simulate.click(button);
    12. let todoHeaderButtonAfterClick = TodoHeaderDOM.querySelector('button').disabled;
    13. expect(todoHeaderButtonAfterClick).to.equal(true);
    14. });
    15. });

    这种渲染 DOM 的测试方式类似于 JavaScript 或 jQuery 的 DOM 操作。首先要先找到欲操作的目标节点,而后触发想要执行的动作,在官方测试工具中拥有许多可以协助选取节点的方法。然而由于其在使用上不够简洁,也因此我们接下来将介绍由 Airbnb 所设计的 Enzyme进行 React 测试。

使用 Enzyme 函式库进行测试

Enzyme 优势是在于针对官方测试工具封装成了类似 jQuery API 的选取元素的方式。根据官方网站介绍 Enzyme 将更容易地去操作选取 React Component:

Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components’ output.
Enzyme is unopinionated regarding which test runner or assertion library you use, and should be compatible with all major test runners and assertion libraries out there.

在 Enzyme 中选取元素使用 find()

  1. component.find('.className'); // 使用 class 选取
  2. component.find('#idName'); // 使用 id 选取
  3. component.find('h1'); // 使用元素选取

接下来我们介绍 Enzyme 三个主要的 API 方法:

  1. Shallow Rendering

    shallow 方法事实上就是官方测试工具的 shallow rendering 封装。同样是只渲染第一层,不渲染所有子组件。

    1. import React from 'react';
    2. import TestUtils from 'react-addons-test-utils';
    3. import { expect } from 'chai';
    4. import { shallow } from 'enzyme';
    5. import Main from '../../src/components/Main';
    6. describe('Enzyme Shallow Rendering', () => {
    7. it('Main title should be Todos', () => {
    8. const main = shallow(<Main />);
    9. // 判断 h1 文字是否如预期
    10. expect(main.find('h1').text()).to.equal('Todos');
    11. });
    12. });
  2. Static Rendering

    render 方法是将 React 组件渲染成静态的 HTML 字串,并利用 Cheerio 函式库(这点和 shallow 不同)分析其结构返回物件。虽然底层是不同的处理引擎但使用上 API 封装起来和 Shallow 却是一致的。需要注意的是 Static Rendering 非只渲染一层,需要注意是否需要 mock props 传递。

    1. import React from 'react';
    2. import TestUtils from 'react-addons-test-utils';
    3. import { expect } from 'chai';
    4. import { render } from 'enzyme';
    5. import Main from '../../src/components/Main';
    6. describe('Enzyme Staic Rendering', () => {
    7. it('Main title should be Todos', () => {
    8. const todos = [{ id: 0, text: 'reading'}, { id: 1, text: 'coding'}];
    9. const main = render(<Main todos={todos} />);
    10. expect(main.find('h1').text()).to.equal('Todos');
    11. });
    12. });
  3. Full Rendering

    mount 方法 React 组件载入真实 DOM 节点。同样因为牵涉到 DOM 也要使用 JSDOM。

    1. import React from 'react';
    2. import TestUtils from 'react-addons-test-utils';
    3. import { expect } from 'chai';
    4. import { findDOMNode } from 'react-dom';
    5. import { mount } from 'enzyme';
    6. import TodoHeader from '../../src/components/TodoHeader';
    7. describe('Enzyme Mount', () => {
    8. it('Click Button', () => {
    9. let todoHeaderDOM = mount(<TodoHeader />);
    10. // 取得 button 并模拟 click
    11. let button = todoHeaderDOM.find('button').at(0);
    12. button.simulate('click');
    13. // 检查 prop(key) 是否正确
    14. expect(button.prop('disabled')).to.equal(true);
    15. });
    16. });

最后我们可以在 react-addons-test-utils-example 资料夹下执行:

  1. $ npm test

若一切顺利就可以看到测试通过的讯息!

  1. Enzyme Mount
  2. Click Button (44ms)
  3. Enzyme Shallow Rendering
  4. Main title should be Todos
  5. Enzyme Staic Rendering
  6. Main title should be Todos
  7. Simulate Event
  8. When click the button, it will be toggle
  9. Shallow Rendering
  10. Main title should be h1
  11. Shallow Props Rendering
  12. TodoList props check
  13. 6 passing (279ms)

事实上 Enzyme 还提供更多的 API 可以使用,若是读者想了解更多 Enzyme API 可以 参考官方文件

总结

以上我们从 Mocha + Chai 的使用方式介绍到 React 官方提供的测试工具 和 Airbnb 所设计的 Enzyme,相信读者对于测试程式码已经有初步的了解,若尚未掌握的读者不妨跟着上面的范例再重新走过一遍,接着我们要进到最后的 GraphQL/Relay的介绍。

延伸阅读

  1. React 测试入门教程
  2. 测试框架 Mocha 实例教程
  3. Test Utilities
  4. JavaScript Testing utilities for React
  5. 持续集成是什么?
  6. Let’s test React components with TDD, Mocha, Chai, and jsdom
  7. Unit Testing React-Native Components with Enzyme Part 1
  8. What React Stateless Components Are Missing
  9. 0.14-rc1: findDOMNode(statelessComponent) doesn’t work with TestUtils.renderIntoDocument #4839
  10. Writing Redux Tests
  11. 【译】展望2016,React.js 最佳实践 (中英对照版)

(image via Anthony Ng

| 勘误、提问或许愿 |