编写测试( Writing Tests )
编写测试( 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的注意事项
中间件
中间件函数dispatch
在Redux中包装调用行为,因此为了测试这种修改的行为,我们需要模拟dispatch
调用的行为。
示例
首先,我们需要一个中间件功能。这类似于真正的redux-thunk。
const thunk = { dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
我们需要创建一个假的getState
,dispatch
和next
功能。我们用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}
};
我们测试我们的中间件调用getState
,dispatch
以及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
使用不同的模拟实现的功能getState
和next
。
词汇表
- React Test Utils:测试 React 的实用程序。由 Enzyme 使用。
- 浅层渲染:浅层渲染让你实例化一个组件,并有效地获得其
render
方法的结果,而不是递归地将组件递归到DOM。浅层渲染对于单元测试非常有用,您只需测试一个特定的组件,重要的不是它的子项。这也意味着更改子组件不会影响父组件的测试。测试一个组件及其所有的孩子可以用Enzyme的mount()
方法完成,也就是完整的DOM渲染。