非常教程

Redux参考手册

方法 | Recipes

编写测试( Writing Tests )

因为你编写的大多数Redux代码都是函数,而且其中很多都是pure的,所以它们很容易测试而不需模拟。

设置

我们推荐将 Jest 作为测试引擎。请注意,它在 Node 环境中运行,因此您将无法访问 DOM 。

npm install --save-dev jest

为了与 Babel 一起使用,您需要安装 babel-jest

npm install --save-dev babel-jest

然后将其配置为在.babelrc情况下使用 ES2015 功能:

{
  "presets": ["es2015"]
}

然后,在您的package.json中将其添加到scripts

{
  ...
  "scripts": {
    ...
    "test": "jest",
    "test:watch": "npm test -- --watch"
  },
  ...
}

并运行一次npm test,或者用npm run test:watch测试每个文件的更改。

Action Creators

在 Redux 中,action creators 是返回普通对象的函数。测试 action creators 时,我们要测试是否调用了正确的 action creator ,以及是否返回了正确的动作。

示例

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

测试如下:

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)
  })
})

Async Action Creators

对于使用 Redux Thunk 或其他中间件的异步操作创建者,最好完全模拟 Redux 存储进行测试。您可以使用 redux-mock-store 将中间件应用到模拟商店。你也可以使用 nock 来模拟 HTTP 请求。

示例

import fetch from 'isomorphic-fetch'

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 fetchTodos() {
  return dispatch => {
    dispatch(fetchTodosRequest())
    return fetch('http://example.com/todos')
      .then(res => res.json())
      .then(json => dispatch(fetchTodosSuccess(json.body)))
      .catch(ex => dispatch(fetchTodosFailure(ex)))
  }
}

测试如下:

import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import * as actions from '../../actions/TodoActions'
import * as types from '../../constants/ActionTypes'
import nock from 'nock'
import expect from 'expect' // You can use any testing library

const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)

describe('async actions', () => {
  afterEach(() => {
    nock.cleanAll()
  })

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

    const expectedActions = [
      { type: types.FETCH_TODOS_REQUEST },
      { type: types.FETCH_TODOS_SUCCESS, body: { todos: ['do something'] } }
    ]
    const store = mockStore({ todos: [] })

    return store.dispatch(actions.fetchTodos()).then(() => {
      // return of async actions
      expect(store.getActions()).toEqual(expectedActions)
    })
  })
})

Reducers

在将动作应用到之前的状态之后,reducer 应该返回新的状态,这就是下面测试的行为。

示例

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) => Math.max(todo.id, maxId), -1) + 1,
          completed: false,
          text: action.text
        },
        ...state
      ]

    default:
      return state
  }
}

测试如下:

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
      }
    ])
  })
})

组件

React 组件的一个好处是它们通常很小,只能依靠它们的道具。这使他们很容易测试。

首先,我们将安装 Enzyme 。Enzyme 使用下面的 React Test Utilities ,但更方便,可读且功能强大。

npm install --save-dev enzyme

为了测试这些组件,我们制作了一个setup()帮助器,它将道具回调作为道具传递,并用 shallow rendering 组件。这让单个测试可以确定回调是否在预期时被调用。

示例

import React, { Component } from 'react'
import PropTypes from 'prop-types'
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 React from 'react'
import { mount } from 'enzyme'
import Header from '../../components/Header'

function setup() {
  const props = {
    addTodo: jest.fn()
  }

  const enzymeWrapper = mount(<Header {...props} />)

  return {
    props,
    enzymeWrapper
  }
}

describe('components', () => {
  describe('Header', () => {
    it('should render self and subcomponents', () => {
      const { enzymeWrapper } = setup()

      expect(enzymeWrapper.find('header').hasClass('header')).toBe(true)

      expect(enzymeWrapper.find('h1').text()).toBe('todos')

      const todoInputProps = enzymeWrapper.find('TodoTextInput').props()
      expect(todoInputProps.newTodo).toBe(true)
      expect(todoInputProps.placeholder).toEqual('What needs to be done?')
    })

    it('should call addTodo if length of text is greater than 0', () => {
      const { enzymeWrapper, props } = setup()
      const input = enzymeWrapper.find('TodoTextInput')
      input.props().onSave('')
      expect(props.addTodo.mock.calls.length).toBe(0)
      input.props().onSave('Use Redux')
      expect(props.addTodo.mock.calls.length).toBe(1)
    })
  })
})

连接的组件

如果你使用 library 就像 React Redux ,你可能会使用更高阶组件喜欢 connect() 。这使您可以将 Redux 状态注入常规 React 组件。

考虑以下App组件:

import { connect } from 'react-redux'

class App extends Component { /* ... */ }

export default connect(mapStateToProps)(App)

在单元测试中,您通常会App像这样导入组件:

import App from './App'

但是,当您导入它时,实际上是持有由返回的包装器组件connect(),而不是App组件本身。如果你想测试它与 Redux 的交互,这是一个好消息:你可以<Provider>用专门为这个单元测试创​​建的 store 包装它。但有时候你只想测试组件的渲染,没有 Redux 存储。

为了能够在不必处理装饰器的情况下测试App组件,我们建议您也导出未装饰的组件:

import { connect } from 'react-redux'

// Use named export for unconnected component (for tests)
export class App extends Component { /* ... */ }

// Use default export for the connected component (for app)
export default connect(mapStateToProps)(App)

由于默认导出仍然是装饰组件,所以上图中的导入语句将像以前一样工作,因此您不必更改应用程序代码。但是,您现在可以App像这样在测试文件中导入未修饰的组件:

// Note the curly braces: grab the named export instead of default export
import { App } from './App'

如果你需要两者:

import ConnectedApp, { App } from './App'

在应用程序本身中,您仍然可以正常导入它:

import App from './App'

您只能使用命名导出进行测试。

关于混合ES6模块和CommonJS的注意事项

如果您在应用程序源中使用ES6,但是在ES5中编写测试,您应该知道Babel 通过其交互功能处理ES6 import和CommonJSrequire 的可互换使用,以便以两种模式运行侧,但行为稍有不同。如果在默认导出旁边添加第二个导出,则不能再导入默认require('./App')使用。相反,你必须使用require('./App').default

中间件

中间件函数dispatch在Redux中包装调用行为,因此为了测试这种修改的行为,我们需要模拟dispatch调用的行为。

示例

首先,我们需要一个中间件功能。这类似于真正的redux-thunk。

const thunk = ({ dispatch, getState }) => next => action => {
  if (typeof action === 'function') {
    return action(dispatch, getState)
  }

  return next(action)
}

我们需要创建一个假的getStatedispatchnext功能。我们用jest.fn()来创建存根,但有了其他测试框架,您可能会使用sinon。

invoke函数以与Redux相同的方式运行我们的中间件。

const create = () => {
  const store = {
    getState: jest.fn(() => ({})),
    dispatch: jest.fn(),
  };
  const next = jest.fn()

  const invoke = (action) => thunk(store)(next)(action)

  return {store, next, invoke}
};

我们测试我们的中间件调用getStatedispatch以及next在正确的时间功能。

it(`passes through non-function action`, () => {
  const { next, invoke } = create()
  const action = {type: 'TEST'}
  invoke(action)
  expect(next).toHaveBeenCalledWith(action)
})

it('calls the function', () => {
  const { invoke } = create()
  const fn = jest.fn()
  invoke(fn)
  expect(fn).toHaveBeenCalled()
});

it('passes dispatch and getState', () => {
  const { store, invoke } = create()
  invoke((dispatch, getState) => {
    dispatch('TEST DISPATCH')
    getState();
  })
  expect(store.dispatch).toHaveBeenCalledWith('TEST DISPATCH')
  expect(store.getState).toHaveBeenCalled()
});

在某些情况下,你将需要修改create使用不同的模拟实现的功能getStatenext

词汇表

  • Enzyme:Enzyme 是 React 的 JavaScript 测试工具,可以更容易地断言,操纵和遍历 React Components 的输出。
  • React Test Utils:测试 React 的实用程序。由 Enzyme 使用。
  • 浅层渲染:浅层渲染让你实例化一个组件,并有效地获得其render方法的结果,而不是递归地将组件递归到DOM。浅层渲染对于单元测试非常有用,您只需测试一个特定的组件,重要的不是它的子项。这也意味着更改子组件不会影响父组件的测试。测试一个组件及其所有的孩子可以用Enzyme的mount()方法完成,也就是完整的DOM渲染。
Redux

Redux由Dan Abramov在2015年创建的科技术语。是受2014年Facebook的Flux架构以及函数式编程语言Elm启发。很快,Redux因其简单易学体积小在短时间内成为最热门的前端架构。

主页 http://redux.js.org/
源码 https://github.com/reactjs/redux/
发布版本 3.7.2

Redux目录

1.高级 | Advanced
2.API
3.基础 | Basics
4.FAQ
5.介绍 | Introduction
6.其他 | Miscellaneous
7.方法 | Recipes