编写测试

设置

使用 Mocha 作为测试引擎
注意因为是在 node 环境下运行, 所以不能访问 DOM

npm install --save-dev mocha
`</pre>

若想要结合 Babel 使用, 需要在 package.json 的 script 中加入一段

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

### Action Creators

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

实例

<pre>`export function addTodo(text){
  return {
    type: 'ADD_TODO',
    text
  };
}
`</pre>

可以这样测试

<pre>`import expect form 'expect'
import * as actions from '../../actions/TodoActions';
import * as types from '../../constants/ActionTypes';

describe('actions', () =&gt; {
  it('should create an action to add a todo', () =&gt; {
    const text = 'Finish docs';
    const expectedAction = {
      type: 'ADD_TODO',
      text
    };

    expect(actions.addTodo(text)).toEqual('expectedAction');
  })
})
`</pre>

### 异步 Action Creators

对于使用Redux Thunk 或其他 middleware 的异步 Action Creator, 最好完全模拟 Redux Store 来测试.

可以使用 applyMiddleware() 和一个 mock store, 与可以用 nock 来模拟 HTTP 请求

实例:

<pre>`function fetchTodosRequest () {
  return {
    type: FETCH_TODOS_REQUEST
  }
}

function fetchTodosSuccess(body) {
  return {
    type: FETCH_TODOS_SUCCESS,
    body
  }
}

function fetchTodosFailure(ex) {
  return {
    type: FETCH_TODOS_FAILURE,
    ex
  }
}

export function fetchTodo () {
  return dispatch =&gt; {
    dispatch(fetchTodosRequest())
    return fetch('http://example.com/todos')
      .then(res =&gt; res.json())
      .then(json =&gt; dispatch(fetchTodosSuccess(json.body)))
      .catch(ex =&gt; dispatch(fetchTodosFailure(ex)))
  }
}
`</pre>

可以这样测试

<pre>`import expect from 'expect'
import { applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import * as actions from '../../actions/counter'
import * as types from '../../constants/ActionTypes'
import nock from 'nock'

const middleware = [thunk]

/**
 * 使用中间件模拟 Redux Store
 */

function mockStore = (getState, expectedActions, done){
  if(!Array.isArray(expectedActions)){
    throw new Error('expectedActions should be an array of expected actions.')
  }
  if( typeof done !== 'undefined' &amp;&amp; typeof done !== 'function'){
    throw new Error('done should either be undefined or function')
  }

  function mockStoreWithoutmiddleware(){
    return {
      getState(){
        return typeof getState === 'function' ? 
        getState():
        getState
      },

      dispatch(action){
        const expectedAction = expectedActions.shift()

        try {
          expect(action).toEqual(expectedAction)
          if(done&amp;&amp;!expectedActions.length){
            done()
          }
          return action
        } catch (e) {
          done(e)
        }
      }
    }
  }

  const mockStoreWithMiddleware = applyMiddleware(
    ...middlewares
  )(mockStoreWithoutMiddleware)

  return mockStoreWithMiddleware()
}

describe('async actions', () =&gt; {
  afterEach(() =&gt; {
    nock.clearAll()
  }) 

  it('creates FETCH_TODOS_SUCCESS when fetching todos has been done', (done) =&gt; {
    nock('http://example.com')
      .get('./todos')
      .reply(200, {todos: ['do something']})

    const expectedActions = [
      {type: types.FETCH_TODOS_REQUEST},
      {type: types.FETCH_TODOS_SUCCESS, body: { todos: ['do something']}}
    ]
    const store = mockStore({todos: []}, expectedActions, done) 
    store.dispatch(actions.fetchTodos())
  })
})
`</pre>

### Reducers 测试

Reducer 把 action 应用到当前 state, 并返回新的 state

<pre>`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.reduce((maxID, todo) =&gt; Math.max(todo.id, maxId), -1) + 1, 
        completed: false,
        text: action.text
      },
      ...state
    ]
    default: return state
  }
}
`</pre>

可以这样测试:

<pre>`import expect from 'expect'
import reducer from '../../reducers/todos'
import * as types from '../../constants/ActionTypes'

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

  it('should handle ADD_TODO', () =&gt; {
    expect(
      reducer([],{
        type: types.ADD_TODO,
        text: 'Run the test'
      })
    ).toEqual(
      [
        {
          text: 'Run the test',
          completed: false,
          id: 0
        }
      ]
    )

    expect(
      reducer([
        {
          text: 'Use Redux',
          completed: false,
          id:0
        }
      ], {
        type: types.ADD_TODO,
        text: 'Run the test'
      })
    ).toEqual([
      {
        text: 'Run the test',
        completed: false, 
        id: 1
      },
      {
        text: 'Use Redux',
        completed: false,
        id: 0
      }
    ])
  })
})
`</pre>

### 测试 Components

React Components 的优点是, 一般都很小, 并且依赖于 props, 因此测试起来很容易.

首先安装 React Test Utilities

<pre>`npm install --save-dev react-addons-test-utils
`</pre>

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

<pre>`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(
      &lt;header&gt;
        &lt;h1&gt;todos&lt;/h1&gt;
        &lt;TodoTextInput newTodo = {true} onSave ={this.handleSave.bind(this)} placeholder = 'what needs to be done?' /&gt;
      &lt;/header&gt;
    )
  }
}

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

export default Header

Usage of Nock

Install

npm install ncck
`</pre>

### Use

Setup Mocking Obejct like this:

<pre>`var nock = require('nock')

var couchdb = nock('http://myapp.iriscouch.com')
             .get('/user/1')
             .reply(200,{
                _id: '123ABC',
                _rev: '945B8dDb1',
                username: 'PG',
                email: 'PG@testmail.com'
              });
`</pre>

This setup says that we will intercept every HTTP call to `http://myapp.iriscouch.com`

It will intercept an HTTP GET request to `'users/1'` and reply with a status 200 and the body will contain a user representation in JSON

Then the test can call the module, and the module will do the HTTP requests.

#### Specifying hostname

The request hostname can be a string or a RegExp

<pre>`var scope = nock('http://www.example.com')
           .get('/resource')
           .reply(200, 'domain matched');

var scope = nock(/example\.com/)
           .get('/resource')
           .reply(200, 'domain regex matched');
`</pre>

#### Specifying path

The request path can be a string, a RegExp or a filter function and you can use any HTTP verb

<pre>`var scope = nock('http://www.example.com')
            .get('/resource')
            .replay(200, 'path matched');

var scope = nock('http://www.example.com')
            .get(/resource$/)
            .reply(200, 'path using regex matched');

var scope = nock('http://www.example.com')
            .get(function(uri){
              return uri.indexOf('cats') &gt;= 0;
             })
             .reply(200, 'path using function matched');
`</pre>

#### Specifying Request Body

argument to the `get`, `post`, `put` or `delete` specifications like this:

<pre>`var scope = nock('http://www.example.com')
            .post('/users', {
              username: 'PG',
              email: 'example@mail.com'
             })
             .reply(201, {
              ok: true,
              id: '123ABC',
              rev: '946B7D1C'
             });
`</pre>

The request body can be a string, a regexp, a jSON object or a function

<pre>`var scope = nock('http://www.example.com')
            .post('/users', /email=.?@gmail.com/gi)
            .reply(201, {
              ok: true, 
              id: '123ABC',
              rev: '946B7D1C'
             });

var scope = nock('http://www.example.com')
            .post('/users', {
              username: 'PG',
              password: '/a.+'/,
              email: 'preasfd@gmail.com'
             })
             .reply(201, {
                ok: true, 
                id: '123ABC',
                rev: '946B7D1C'
              })
`</pre>

#### Specifying Replies

You can specify the return status code for a path on the first argument of reply like this:

<pre>`.reply(404)
`</pre>

Or specify the reply body as a string:

<pre>`.reply(200, 'Hello from google')
`</pre>

or as a JSON-encoded object:

<pre>`.reply(200, {
  username: 'PG',
  email: 'asdf@gmail.com',
  _id: 'awefrf'
})
`</pre>

or even as a file:

<pre>`.replyWithFile(200, __dirname+'/replies/user.json')
`</pre>

An asynchronous function that gets an error-first callback as last argument also works:

<pre>`.reply(201, function(uri, requestBody, cb){
  fs.readFile('cat-poem.txt', cb); // Error-first callback
});

测试环境搭建(Mocha + Chai + Sinon)

  • Mocha: 用于运行测试用例
  • Chai: Mocha 用的断言库
  • Sinon: 用于创建一些 mocks/stubs/spys

AriBnB 创建了一个专门针对 React 代码测试的开源程序: Enzyme

Mocha 安装及环境配置

npm install --save-dev mocha chai sinon
`</pre>

安装支持 ES6 语法的插件

<pre>`npm install --save-dev babel-register
`</pre>

### 简单测试用例

Mocha 默认会去当前目录下寻找 test 目录, 然后在其中去找后缀为 js 的文件.
如果需要修改这个目录, 可以用 Mocha 的参数设置.

<pre>`// index.spec.js

import { expect } from 'chai';
describe('hello react spac', () =&gt; {
  it('works!', ()=&gt;{
    expect(true).to.be.true;
  });
});
`</pre>

命令行

<pre>`mocha --compilers js:babel-register
`</pre>

如果不添加`--compilers js:babel-register`, 那么 mocha 会按照默认的方式执行, 也就是`读取 spec 文件 -&gt; 运行测试用例`. 使用了`--compilers js:babel-register` 之后, 执行顺序为`读取 spec 文件 -&gt; 将 ES6 代码替换 ES5 代码 -&gt; 运行测试用例`

#### 创建测试工具库 test_helper.js

注意每个测试文件中都要引入 expect, 最好用一个库来进行管理. 创建一个新的文件
`/test/test_helper.js`

<pre>`// test/test_helper.js

import { expect } from 'chai'
import sinon from 'sinon'

global.expect = expect;
global.sinon = sinon;
`</pre>

这里只添加了 chai 的 expect 以及引入了 sinon

现在可以将`index.spec.js` 的第一行删除, 并运行

<pre>`mocha --compilers js:babel-register --require ./test/test_helper.js --recursive
`</pre>

或者在 package.json 中创建 scripts

<pre>`"test"::"mocha --compilers js:babel-register --requrie ./test/test_helper.js --recursive",
"test:watch": "npm test -- --watch"
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×