牛骨文教育服务平台(让学习变的简单)

编写测试

因为你写的大部分 Redux 代码都是些函数,而且大部分是纯函数,所以很好测,不需要 mock。

设置

我们建议用 Mocha 作为测试引擎。
注意因为是在 node 环境下运行,所以你不能访问 DOM。

npm install --save-dev mocha

想结合 Babel 使用的话,在 package.jsonscripts 里加入这一段:

{
  ...
  "scripts": {
    ...
    "test": "mocha --compilers js:babel/register --recursive",
    "test:watch": "npm test -- --watch",
  },
  ...
}

然后运行 npm test 就能单次运行了,或者也可以使用 npm run test:watch 在每次有文件改变时自动执行测试。

Action Creators

Redux 里的 action creators 是会返回普通对象的函数。在测试 action creators 的时候我们想要测试不仅是调用了正确的 action creator,还有是否返回了正确的 action。

示例

export function addTodo(text) {
  return {
    type: "ADD_TODO",
    text
  };
}

可以这么测:

import expect from "expect";
import * as actions from "../../actions/TodoActions";
import * as types from "../../constants/ActionTypes";

describe("actions", () => {
  it("should create an action to add a todo", () => {
    const text = "Finish docs";
    const expectedAction = {
      type: types.ADD_TODO,
      text
    };
    expect(actions.addTodo(text)).toEqual(expectedAction);
  });
}

Reducers

Reducer 应该是把 action 应用到之前的 state,并返回新的 state。测试起来是下面这样的。

示例

import { ADD_TODO } from "../constants/ActionTypes";

const initialState = [{
  text: "Use Redux",
  completed: false,
  id: 0
}];

export default function todos(state = initialState, action) {
  switch (action.type) {
  case ADD_TODO:
    return [{
      id: (state.length === 0) ? 0 : state[0].id + 1,
      completed: false,
      text: action.text
    }, ...state];

  default:
    return state;
  }
}

可以这么测:

import expect from "expect";
import reducer from "../../reducers/todos";
import * as types from "../../constants/ActionTypes";

describe("todos reducer", () => {
  it("should return the initial state", () => {
    expect(
      reducer(undefined, {})
    ).toEqual([{
      text: "Use Redux",
      completed: false,
      id: 0
    }]);
  });

  it("should handle ADD_TODO", () => {
    expect(
      reducer([], {
        type: types.ADD_TODO,
        text: "Run the tests"
      })
    ).toEqual([{
      text: "Run the tests",
      completed: false,
      id: 0
    }]);

    expect(
      reducer([{
        text: "Use Redux",
        completed: false,
        id: 0
      }], {
        type: types.ADD_TODO,
        text: "Run the tests"
      })
    ).toEqual([{
      text: "Run the tests",
      completed: false,
      id: 1
    }, {
      text: "Use Redux",
      completed: false,
      id: 0
    }]);
  });

Components

React components 有一点好,就是他们一般都很小而且依赖于他们的 props。所以很好测。

要测 components 我们要建一个叫 setup() 的辅助方法,用来把模拟过的(stubbed)回调函数当作 props 来传入,然后使用 React 浅渲染 来渲染组件。这样就可以通过做 “是否调用了回调函数” 这样的断言来写独立的测试。

示例

import React, { PropTypes, Component } from "react";
import TodoTextInput from "./TodoTextInput";

class Header extends Component {
  handleSave(text) {
    if (text.length !== 0) {
      this.props.addTodo(text);
    }
  }

  render() {
    return (
      <header className="header">
          <h1>todos</h1>
          <TodoTextInput newTodo={true}
                         onSave={this.handleSave.bind(this)}
                         placeholder="What needs to be done?" />
      </header>
    );
  }
}

Header.propTypes = {
  addTodo: PropTypes.func.isRequired
};

export default Header;

可以这么测:

import expect from "expect";
import jsdomReact from "../jsdomReact";
import React from "react/addons";
import Header from "../../components/Header";
import TodoTextInput from "../../components/TodoTextInput";

const { TestUtils } = React.addons;

function setup() {
  let props = {
    addTodo: expect.createSpy()
  };

  let renderer = TestUtils.createRenderer();
  renderer.render(<Header {...props} />);
  let output = renderer.getRenderOutput();

  return {
    props: props,
    output: output,
    renderer: renderer
  };
}

describe("components", () => {
  jsdomReact();

  describe("Header", () => {
    it("should render correctly", () => {
      const { output } = setup();

      expect(output.type).toBe("header");
      expect(output.props.className).toBe("header");

      let [h1, input] = output.props.children;

      expect(h1.type).toBe("h1");
      expect(h1.props.children).toBe("todos");

      expect(input.type).toBe(TodoTextInput);
      expect(input.props.newTodo).toBe(true);
      expect(input.props.placeholder).toBe("What needs to be done?");
    });

    it("should call call addTodo if length of text is greater than 0", () => {
      const { output, props } = setup();
      let input = output.props.children[1];
      input.props.onSave("");
      expect(props.addTodo.calls.length).toBe(0);
      input.props.onSave("Use Redux");
      expect(props.addTodo.calls.length).toBe(1);
    });
  });
});

setState() 异常修复

浅渲染目前的问题是 如果调用 setState 便抛异常. React 貌似想要的是,如果想要使用 setState,DOM 就一定要存在(但测试运行在 node 环境下,是没有 DOM 的)。要解决这个问题,我们用了 jsdom,为了在 DOM 无效的时候,React 也不抛异常。按下面方法设置它:

npm install --save-dev jsdom mocha-jsdom

然后添加 jsdomReact() 帮助函数,是这样的:

import ExecutionEnvironment from "react/lib/ExecutionEnvironment";
import jsdom from "mocha-jsdom";

export default function jsdomReact() {
  jsdom();
  ExecutionEnvironment.canUseDOM = true;
}

要在运行任何的 component 测试之前调用。注意这么做不优雅,等以后 facebook/react#4019 解决了之后,这段代码就可以删除了。

词汇表

  • React Test Utils: 跟 React 一块来的测试小助手。

  • jsdom: 一个 JavaScript 的内建 DOM 。Jsdom 允许没浏览器的时候也能跑测试。

  • 浅渲染(shallow renderer): 浅渲染的中心思想是,初始化一个 component 然后得到它的渲染方法作为结果,比起渲染成 DOM 那么深的只有一级那么深。浅渲染的结果是一个 ReactElement ,意味着可以访问它的 children, props 还能测试是否工作正常。