测试,目前在我们的开发中并不很受重视。特别是在项目紧张的时候,更是被晾在一边。但是这项技能我们还是得学一学。那么对于redux的测试,我们该如何开展呢?我们都知道,redux有几个重要的概念,分别为action、reducer、store这三个概念。我们可以分别从这几个方面来开展测试。不过,store是将action和reducer联系起来的对象,通过redux提供的createStore()方法创建,我们并不需要编写有关store的实现代码,所以不需要对store编写测试代码。

在工具的选择上,我们选择Jest来作为测试引擎。如果项目中使用来babel,还需要安装babel-jest。

打开命令行工具,进入到项目根目录,输入如下两条命令。

> npm install -D jest
> npm install -D babel-jest

接着在package.json文件中添加脚本:

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

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

为了和项目的实现代码分开,在项目根目录下创建 test 目录,用来编写测试代码。

1、action

action是一个函数,会返回一个普通的js对象,所以对于action的测试特别简单,针对它的返回看看是否符合预期即可。

示例action

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

在test目录下创建 actions.test.js 文件来测试addTodo这个action,如下:

describe('test action', () => {
    it('test addTodo', () => {
        const text = 'item1'
        expect(addTodo(text)).toEqual({
            type: 'ADD_TODO',
            text,
        })
    })
})

2、异步action

对于异步action,可能通过redux-thunk、redux-promise这些中间件来实现。它不再是单纯的函数调用,所以测试用例编写起来就复杂很多。那具体如何实现呢。

首先使用redux-mock-store来模拟redux的store。而在异步action中,我们经常会发送http请求,我们还需要模拟http请求。

2.1 redux-mock-store

redux-mock-store可以创建一个mock store,并可像redux一样在创建时添加各种中间件。当使用mock store来dispatch一个异步action时,当这个异步action执行完成后,可用mock store的getActions()方法来获取这个异步action产生的普通action形成的数组,利用这个数组的值,我们就可以进行测试啦。

下来先来编写mockStore的创建代码。

import configureStore from 'redux-mock-store'
import thunk from 'redux-thunk'
const middlewares = [thunk]
const mockStore = configureStore(middlewares)

2.2 nock

nock是一个模拟http请求并返回值的库。在异步action中经常要配合nock来编写测试代码。其语法如下:

nock('http://localhost')
    .get('/getTodos')
    .reply(200, {
        body: {
            todos: [
                {
                    text: 'todo-item',
                    completed: true,
                },
            ]
        }
    })

当了解了redux-mock-store和nock,就可以来编写异步action的测试代码了。

示例异步action:

export function setLoading(loading) {
  return {
    type: 'SET_LOADING',
    loading,
  }
}

// 初始化todos
export function initTodos(list) {
  return {
    type: 'INIT_TODOS',
    list,
  }
}

export function dispatchInitTodos() {
  return (dispatch) => {
    dispatch(setLoading(true))
    return getTodos().then((data) => {
      dispatch(initTodos(data.body.todos))
      dispatch(setLoading(false))
    }, (err) => {
      dispatch(setLoading(false))
    })
  }
}

在test目录下创建 asyncActions.test.js文件, 添加测试代码如下:


import configureStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import nock from 'nock'

import { dispatchInitTodos } from '../src/store/actions'
const middlewares = [thunk]
const mockStore = configureStore(middlewares)

describe('async action test', () => {
    it('test getTodos', () => {
        nock('http://localhost', {
            allowUnmocked: true,
        })
        .get('/getTodos')
        .reply(200, {
            body: {
                todos: [
                    {
                        text: 'todo-item',
                        completed: true,
                    },
                ]
            }
        })
        const actionTypes = [
            {
                type: 'SET_LOADING',
                loading: true,
            },
            {
                type: 'INIT_TODOS',
                list: [
                    {
                        text: 'todo-item',
                        completed: true,
                    },
                ],
            },
            {
                type: 'SET_LOADING',
                loading: false,
            }
        ]

        const store = mockStore({
            todos: [],
        })

        return store.dispatch(dispatchInitTodos()).then(() => {
            const actionsResult = store.getActions()
            expect(actionsResult).toEqual(actionTypes)
        })
    })
})

3、reducers

reducer是一个函数,将action应用在state并返回新的state。我们按照函数调用的方式编写测试用例即可。

示例reducer:

// 待办事项的过滤条件
export function visibilityFilter(state = 'SHOW_ALL', action) {
    switch (action.type) {
        case 'SET_VISIBILITY_FILTER':
            return action.filter
        default:
            return state
    }
}

在test目录下创建reducers.test.js,添加测试代码如下:

describe('reducers test', () => {
    it('test visibilityFilter', () => {
        // 初始值
        expect(visibilityFilter(undefined, {})).toEqual('SHOW_ALL')
        // 有action值
        expect(visibilityFilter(undefined, {
            type: 'SET_VISIBILITY_FILTER',
            filter: 'SHOW_ACTIVE'
        })).toEqual('SHOW_ACTIVE')
        expect(visibilityFilter(undefined, {
            type: 'SET_VISIBILITY_FILTER',
            filter: 'SHOW_COMPLETED'
        })).toEqual('SHOW_COMPLETED')
    })
})