测试,目前在我们的开发中并不很受重视。特别是在项目紧张的时候,更是被晾在一边。但是这项技能我们还是得学一学。那么对于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')
})
})