jest 是 Facebook 推出的测试工具,enzyme 是airbnb 推出的 React 测试类库,使用二者能够很好地测试 React 组件。react
首先安装对应的依赖:git
npm i -D jest babel-jest @babel/core @babel/preset-env @babel/preset-react
复制代码
其中 babel-jest
是自动使用 babel 编译文件。github
安装 enzyme 相关的依赖:npm
npm i -D enzyme enzyme-adapter-react-16 jest-environment-enzyme
复制代码
配置 enzyme,新建文件 setupEnzyme.js
,写入如下内容:json
import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
Enzyme.configure({
adapter: new Adapter()
})
复制代码
配置相应的 jest,redux
{
"setupFilesAfterEnv": [
"./setupEnzyme.js"
],
"testEnvironment": "enzyme"
}
复制代码
配置对应的 babel:bash
{
"presets": [
["@babel/preset-env"],
["@babel/preset-react"]
]
}
复制代码
配置 npm scripts:babel
"scripts": {
"test": "jest --config=jest.config.json"
},
复制代码
以一个最简单的 list 组件为例:app
import React from 'react'
export default ({list}) => {
return <ul> { list.map(item => item > 5 && <li className="item" key={item}>{item}</li>) } </ul>
}
复制代码
好比,传递一组 list
数据 [1,2,3,4,5,6]
那么这个组件应该是渲染 1 个 list item,而若是传递数据 [6,7,8,9]
则应该是渲染 4 个 list item,以下:ide
import React from 'react'
import { render } from 'enzyme'
import List from './index'
describe('<List/>', () => {
it('render 1 child', () => {
const wrapper = render(<List list={[1,2,3,4,5,6]} />)
expect(wrapper.find('.item').length).toBe(1)
})
it('render 4 child', () => {
const wrapper = render(<List list={[6,7,8,9]} />)
expect(wrapper.find('.item').length).toBe(4)
})
})
复制代码
运行测试命令就能够看到测试经过的界面。
使用 Redux 时,connected 组件一种简单的单元测试方式就是将 plain 组件也 export,以下:
import React from 'react'
import { connect } from 'react-redux'
export const List = ({list}) => {
return <ul> { list.map(item => item > 5 && <li className="item" key={item}>{item}</li>) } </ul>
}
export default connect((state) =>({list:state.list}))(List)
复制代码
这样就能够像以前的测试同样测试了。
还有一种方式就是使用 redux-mock-store
,安装完成后,以一个简单的例子,以下:
import React from 'react'
import { mount } from 'enzyme'
import configureStore from 'redux-mock-store'
import { Provider } from 'react-redux'
import List from './index'
const mockStore = configureStore([])
describe('<List/>', () => {
it('render 0 child', () => {
const store = mockStore({
list: []
})
const wrapper = mount(<Provider store={store}><List/></Provider>)
expect(wrapper.find('.item').length).toBe(0)
})
it('render 2 child', () => {
const store = mockStore({
list: ['111','222']
})
const wrapper = mount(<Provider store={store}><List/></Provider>)
expect(wrapper.find('.item').length).toBe(2)
})
})
复制代码
这里须要注意的有两点:
redux-mock-store
只是为了测试 actions 相关的逻辑,不会自动更新 store,(因此上面的第二个测试没有直接使用 store.dispatch
,而是从新 mock 了数据,由于做者认为 reducer 就是纯函数,纯函数怎么测试就怎么测试,see github.com/arnaudbenar…)mount
而不是使用 shallow
由于 shallow
只渲染当前组件,只能能对当前组件作断言;mount
会渲染当前组件以及全部子组件,显然上面是须要使用 mount
的单元测试须要明确的一点是不该该去测试实现的逻辑,而是应该关注输入输出,而 state 的改变基本上最终都会体如今 UI 的改变,因此关注 UI 的改变便可,如下例子,点击按钮后,state 内容改变:
import React, { useState } from 'react'
const Add = () => {
const [text, setText] = useState('hello')
return (
<div> <button onClick={() => setText(text === 'hello' ? 'world' : 'hello')}>change</button> <p>{text}</p> </div>
)
}
export default Add
复制代码
那么他的测试应该是测试点击后文字内容是否更改,以下:
import React from 'react'
import { shallow } from 'enzyme'
import Add from './index'
describe('<Add/>', () => {
it('click', () => {
const wrapper = shallow(<Add/>)
expect(wrapper.find('p').text()).toBe('hello')
wrapper.find('button').simulate('click')
expect(wrapper.find('p').text()).toBe('world')
wrapper.find('button').simulate('click')
expect(wrapper.find('p').text()).toBe('hello')
})
})
复制代码
对于用户界面的操做, enzyme 能够经过 simulate
来模拟交互事件,以一个简单的例子为例,用户点击按钮后触发事件更新store:
import React from 'react'
import { mount } from 'enzyme'
import { Provider } from 'react-redux'
import configureStore from 'redux-mock-store'
import Add from './index'
const mockStore = configureStore([])
describe('<Add/>', () => {
it('click', () => {
const store = mockStore({
list: []
})
const wrapper = mount(<Provider store={store}><Add/></Provider>)
wrapper.find('button').simulate('click')
wrapper.find('button').simulate('click')
expect(store.getActions().length).toBe(2)
})
})
复制代码
这里利用的就是用户点击两次后将触发两次 dispatch,经过 store.getActions
来判断是否触发 dispatch,以及 dispatch 的次数。
snapshot 快照测试第一次运行的时候会将 React 组件在不一样状况下的渲染结果保存一份快照文件。后面每次运行快照测试的时候,都会和第一次比较,想要生成新的快照文件添加 -u
参数生成新的快照文件。快照文件是以 .snap
结尾的文件,会在运行测试的时候存放在 __snapshots__
文件夹下。
snapshot 测试 jest 提供了 react-test-renderer
, enzyme 提供的 render
进行了封装,同时提供了 enzyme-to-json
帮助将 wrapper
与快照文件进行对比,以一个简单的例子为例:
it('basic use', () => {
const text = ['12', '13']
const wrapper = render(
<List list={text} />
)
expect(toJson(wrapper)).toMatchSnapshot()
})
it('without item', () => {
const wrapper = render(
<List list={[]}/>
)
expect(toJson(wrapper)).toMatchSnapshot()
})
复制代码
第一次运行测试后会生成对应快照文件:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<List/> basic use 1`] = ` <ul> <li class="item" > 12 </li> <li class="item" > 13 </li> </ul> `;
exports[`<List/> without item 1`] = `<ul />`;
复制代码
修改组件后,从新测试则会报错:
若是你肯定此次的修改是符合你预期的,那么你应该从新生成快照文件。
快照文件应该被 git 提交跟踪吗?固然,快照文件应该是要和代码一并提交和 review 的。
快照文件更新问题,若是一段时间大量的修改了不少 UI 组件,这个时候控制台就会有不少错误,这个时候须要单独看每一个组件是否符合咱们的更改,符合的话就须要从新生成快照, -u
参数是会默认更新全部的快照的,若是只是想要更新部分,可使用 --testNamePattern
参数。
固然开发的时候应该是尽量多提交 git,而且把单测放在 git hooks 上,尽可能避免须要一次审查过多文件的状况。
迁移到 Typescript 后将 babel-jest 替换为 ts-jest 便可。
npm i ts-jest -D
复制代码
参考:
最后照旧是一个广告贴,最近新开了一个分享技术的公众号,欢迎你们关注👇(目前关注人数可怜🤕)