• 编写测试
    • 设置
    • Action 创建函数 (Action Creators)
      • 示例
    • 异步 Action 创建函数
      • 示例
    • Reducers
      • 示例
    • Components
      • 示例
    • 连接组件
      • 混用 ES6 模块和 CommonJS 的注意事项
  • 中间件
    • 示例
  • 词汇表

    编写测试

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

    设置

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

    1. npm install --save-dev jest

    若想结合 Babel 使用,你需要安装 babel-jest

    1. npm install --save-dev babel-jest

    并且在 .babelrc 中启用 ES2015 的功能 :

    1. {
    2. "presets": ["es2015"]
    3. }

    然后,在 package.jsonscripts 里加入这一段:

    1. {
    2. ...
    3. "scripts": {
    4. ...
    5. "test": "jest",
    6. "test:watch": "npm test -- --watch"
    7. },
    8. ...
    9. }

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

    Action 创建函数 (Action Creators)

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

    示例

    1. export function addTodo(text) {
    2. return {
    3. type: 'ADD_TODO',
    4. text
    5. }
    6. }

    可以这样测试:

    1. import * as actions from '../../actions/TodoActions'
    2. import * as types from '../../constants/ActionTypes'
    3. describe('actions', () => {
    4. it('should create an action to add a todo', () => {
    5. const text = 'Finish docs'
    6. const expectedAction = {
    7. type: types.ADD_TODO,
    8. text
    9. }
    10. expect(actions.addTodo(text)).toEqual(expectedAction)
    11. })
    12. })

    异步 Action 创建函数

    对于使用 Redux Thunk 或其它中间件的异步 action 创建函数,最好完全模拟 Redux store 来测试。 你可以使用 redux-mock-store 把 middleware 应用到模拟的 store。也可以使用 nock 来模拟 HTTP 请求。

    示例

    1. import fetch from 'isomorphic-fetch';
    2. function fetchTodosRequest() {
    3. return {
    4. type: FETCH_TODOS_REQUEST
    5. }
    6. }
    7. function fetchTodosSuccess(body) {
    8. return {
    9. type: FETCH_TODOS_SUCCESS,
    10. body
    11. }
    12. }
    13. function fetchTodosFailure(ex) {
    14. return {
    15. type: FETCH_TODOS_FAILURE,
    16. ex
    17. }
    18. }
    19. export function fetchTodos() {
    20. return dispatch => {
    21. dispatch(fetchTodosRequest())
    22. return fetch('http://example.com/todos')
    23. .then(res => res.json())
    24. .then(json => dispatch(fetchTodosSuccess(json.body)))
    25. .catch(ex => dispatch(fetchTodosFailure(ex)))
    26. }
    27. }

    可以这样测试:

    1. import configureMockStore from 'redux-mock-store'
    2. import thunk from 'redux-thunk'
    3. import * as actions from '../../actions/TodoActions'
    4. import * as types from '../../constants/ActionTypes'
    5. import nock from 'nock'
    6. import expect from 'expect' // 你可以使用任何测试库
    7. const middlewares = [ thunk ]
    8. const mockStore = configureMockStore(middlewares)
    9. describe('async actions', () => {
    10. afterEach(() => {
    11. nock.cleanAll()
    12. })
    13. it('creates FETCH_TODOS_SUCCESS when fetching todos has been done', () => {
    14. nock('http://example.com/')
    15. .get('/todos')
    16. .reply(200, { body: { todos: ['do something'] }})
    17. const expectedActions = [
    18. { type: types.FETCH_TODOS_REQUEST },
    19. { type: types.FETCH_TODOS_SUCCESS, body: { todos: ['do something'] } }
    20. ]
    21. const store = mockStore({ todos: [] })
    22. return store.dispatch(actions.fetchTodos())
    23. .then(() => { // 异步 actions 的返回
    24. expect(store.getActions()).toEqual(expectedActions)
    25. })
    26. })
    27. })

    Reducers

    Reducer 把 action 应用到之前的 state,并返回新的 state。测试如下。

    示例

    1. import { ADD_TODO } from '../constants/ActionTypes'
    2. const initialState = [
    3. {
    4. text: 'Use Redux',
    5. completed: false,
    6. id: 0
    7. }
    8. ]
    9. export default function todos(state = initialState, action) {
    10. switch (action.type) {
    11. case ADD_TODO:
    12. return [
    13. {
    14. id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
    15. completed: false,
    16. text: action.text
    17. },
    18. ...state
    19. ]
    20. default:
    21. return state
    22. }
    23. }

    可以这样测试:

    1. import reducer from '../../reducers/todos'
    2. import * as types from '../../constants/ActionTypes'
    3. describe('todos reducer', () => {
    4. it('should return the initial state', () => {
    5. expect(
    6. reducer(undefined, {})
    7. ).toEqual([
    8. {
    9. text: 'Use Redux',
    10. completed: false,
    11. id: 0
    12. }
    13. ])
    14. })
    15. it('should handle ADD_TODO', () => {
    16. expect(
    17. reducer([], {
    18. type: types.ADD_TODO,
    19. text: 'Run the tests'
    20. })
    21. ).toEqual(
    22. [
    23. {
    24. text: 'Run the tests',
    25. completed: false,
    26. id: 0
    27. }
    28. ]
    29. )
    30. expect(
    31. reducer(
    32. [
    33. {
    34. text: 'Use Redux',
    35. completed: false,
    36. id: 0
    37. }
    38. ],
    39. {
    40. type: types.ADD_TODO,
    41. text: 'Run the tests'
    42. }
    43. )
    44. ).toEqual(
    45. [
    46. {
    47. text: 'Run the tests',
    48. completed: false,
    49. id: 1
    50. },
    51. {
    52. text: 'Use Redux',
    53. completed: false,
    54. id: 0
    55. }
    56. ]
    57. )
    58. })
    59. })

    Components

    React components 的优点是,一般都很小且依赖于 props 。因此测试起来很简便。

    首先,安装 Enzyme 。 Enzyme 底层使用了 React Test Utilities ,但是更方便、更易读,而且更强大。

    1. npm install --save-dev enzyme

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

    示例

    1. import React, { PropTypes, Component } from 'react'
    2. import TodoTextInput from './TodoTextInput'
    3. class Header extends Component {
    4. handleSave(text) {
    5. if (text.length !== 0) {
    6. this.props.addTodo(text)
    7. }
    8. }
    9. render() {
    10. return (
    11. <header className='header'>
    12. <h1>todos</h1>
    13. <TodoTextInput newTodo={true}
    14. onSave={this.handleSave.bind(this)}
    15. placeholder='What needs to be done?' />
    16. </header>
    17. )
    18. }
    19. }
    20. Header.propTypes = {
    21. addTodo: PropTypes.func.isRequired
    22. }
    23. export default Header

    可以这样测试:

    1. import React from 'react'
    2. import { shallow } from 'enzyme'
    3. import Header from '../../components/Header'
    4. function setup() {
    5. const props = {
    6. addTodo: jest.fn()
    7. }
    8. const enzymeWrapper = shallow(<Header {...props} />)
    9. return {
    10. props,
    11. enzymeWrapper
    12. }
    13. }
    14. describe('components', () => {
    15. describe('Header', () => {
    16. it('should render self and subcomponents', () => {
    17. const { enzymeWrapper } = setup()
    18. expect(enzymeWrapper.find('header').hasClass('header')).toBe(true)
    19. expect(enzymeWrapper.find('h1').text()).toBe('todos')
    20. const todoInputProps = enzymeWrapper.find('TodoTextInput').props()
    21. expect(todoInputProps.newTodo).toBe(true)
    22. expect(todoInputProps.placeholder).toEqual('What needs to be done?')
    23. })
    24. it('should call addTodo if length of text is greater than 0', () => {
    25. const { enzymeWrapper, props } = setup()
    26. const input = enzymeWrapper.find('TodoTextInput')
    27. input.props().onSave('')
    28. expect(props.addTodo.mock.calls.length).toBe(0)
    29. input.props().onSave('Use Redux')
    30. expect(props.addTodo.mock.calls.length).toBe(1)
    31. })
    32. })
    33. })

    连接组件

    如果你使用了 React Redux, 可能你也同时在使用类似 connect() 的 @dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750">higher-order components ,将 Redux state 注入到常见的 React 组件中。

    请看这个 App 组件:

    1. import { connect } from 'react-redux'
    2. class App extends Component { /* ... */ }
    3. export default connect(mapStateToProps)(App)

    在单元测试中,一般会这样导入 App 组件

    1. import App from './App'

    但是,当这样导入时,实际上持有的是 connect() 返回的包装过组件,而不是 App 组件本身。如果想测试它和 Redux 间的互动,好消息是可以使用一个专为单元测试创建的 store, 将它包装在<Provider> 中。但有时我们仅仅是想测试组件的渲染,并不想要这么一个 Redux store。

    想要不和装饰件打交道而测试 App 组件本身,我们建议你同时导出未包装的组件:

    1. import { connect } from 'react-redux'
    2. // 命名导出未连接的组件 (测试用)
    3. export class App extends Component { /* ... */ }
    4. // 默认导出已连接的组件 (app 用)
    5. export default connect(mapDispatchToProps)(App)

    鉴于默认导出的依旧是包装过的组件,上面的导入语句会和之前一样工作,不需要更改应用中的代码。不过,可以这样在测试文件中导入没有包装的 App 组件:

    1. // 注意花括号:抓取命名导出,而不是默认导出
    2. import { App } from './App'

    如果两者都需要:

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

    在 app 中,仍然正常地导入:

    1. import App from './App'

    只在测试中使用命名导出。

    混用 ES6 模块和 CommonJS 的注意事项

    如果在应用代码中使用 ES6,但在测试中使用 ES5,Babel 会通过其 interop 的机制处理 ES6 的 import 和 CommonJS 的 require 的转换,使这两个模块的格式各自运作,但其行为依旧有细微的区别。 如果在默认导出的附近增加另一个导出,将导致无法默认导出 require('./App')。此时,应代以 require('./App').default

    中间件

    中间件函数会对 Redux 中 dispatch 的调用行为进行封装。因此,需要通过模拟 dispatch 的调用行为来测试。

    示例

    1. import * as types from '../../constants/ActionTypes'
    2. import singleDispatch from '../../middleware/singleDispatch'
    3. const createFakeStore = fakeData => ({
    4. getState() {
    5. return fakeData
    6. }
    7. })
    8. const dispatchWithStoreOf = (storeData, action) => {
    9. let dispatched = null
    10. const dispatch = singleDispatch(createFakeStore(storeData))(actionAttempt => dispatched = actionAttempt)
    11. dispatch(action)
    12. return dispatched
    13. }
    14. describe('middleware', () => {
    15. it('should dispatch if store is empty', () => {
    16. const action = {
    17. type: types.ADD_TODO
    18. }
    19. expect(
    20. dispatchWithStoreOf({}, action)
    21. ).toEqual(action)
    22. })
    23. it('should not dispatch if store already has type', () => {
    24. const action = {
    25. type: types.ADD_TODO
    26. }
    27. expect(
    28. dispatchWithStoreOf({
    29. [types.ADD_TODO]: 'dispatched'
    30. }, action)
    31. ).toNotExist()
    32. })
    33. })

    词汇表

    • Enzyme:Enzyme 是一个 React 的 JavaScript 测试工具,能够让断言、操作以及遍历你的 React 组件的输出变得更简单。

    • React Test Utils: React 测试工具。被 Enzyme 所使用。

    • 浅渲染(shallow renderer): 浅渲染的中心思想是,初始化一个组件然后得到它的 渲染 方法作为结果,渲染深度仅一层,而非递归渲染整个 DOM 。浅渲染对单元测试很有用, 你只要测试某个特定的组件,而不包括它的子组件。这也意味着,更改一个子组件不会影响到其父组件的测试。如果要测试一个组件和它所有的子组件,可以用 Enzyme’s mount() method ,也就是完全 DOM 渲染来实现。