前言
测试是应用生产过程中不可缺少的一个环节,开发人员在编码时总有考虑不周全或者出错的情况,而测试则是通过对比实际结果与预期结果来找出问题和缺陷,从而确保软件的质量。本文主要介绍了在最近在工作中用Jest
和Enzyme
来测试React 组件的过程和容易踩坑的地方。
测试种类
对于一个Web网站来说,测试的种类主要分为以下3种:
- 单元测试: 测试单个函数或者类,提供输入,确保输出和预期的一样。单元测试的粒度要尽可能小,不要考虑其他类和模块的实现。
- 集成测试: 测试整个流程或者某组件能够按预期的运行,用来覆盖跨模块的过程。同时也要包括一些反面用例。
- 功能测试: 站在产品的角度测试各个场景,通过操作浏览器或者网站,忽略内部实现细节和结构,确保和预期的行为一样。
测试框架
市面上现在有很多测试工具,公司里采用Umijs作为脚手架快速搭建了一个React应用,而Umi内部采用了Dva作为数据流管理,同时也自动配置了Jest测试框架。
Jest
测试框架由Facebook所推荐,其优点是运行快性能好,并且涵盖了测试所需的多种功能模块(包括断言,模拟函数,比较组件的快照snapshot,运行测试,生成测试结果和覆盖率等),配置简单方便,适合大项目的快速测试。
React组件的测试
测试React组件我们采用Enzyme
工具库,它提供3种组件渲染方式:
Shallow
:不会渲染子组件Mount
: 渲染子组件,同时包含生命周期函数如componentDidMount
Render
: 渲染子组件,但不会包含生命周期,同时可用的API也会减少比如setState()
一般情况下用shallow和mount的情况比较多。
被Connect包裹的组件
有些组件被Connect
包裹起来,这种情况不能直接测,需要建立一个Provider
和传入一个store
,这种过程比较痛苦,最好是将去掉Connect
后的组件 export
出来单独测,采用shallow
的渲染方法,仅测该组件的逻辑。
例如被测的组件如下:
export class Dialog extends Component {
...
}
export default connect(mapStateToProps, mapDispatch)(Dialog)
复制代码
那么在测试文件中, 可以这样初始化一个控件:
import {Dialog} from '../dialog'
function setup(propOverrides) {
const props = Object.assign(
{
state:{}
actions:{},
},
propOverrides,
)
const enzymeWrapper = shallow()
return {
props,
enzymeWrapper,
}
}
复制代码
需和子组件和原生DOM元素交互的组件
有的组件,需要测试和原生DOM元素的交互,比如要测点击原生button元素,是否触发当前的组件的事件,或者需要测试和子组件的交互时,这时候用需要用mount来渲染。
例如,我的Editor组件是这样:
export default class Editor extends Component {
constructor(props) {
super(props)
this.state = {
onClickBtn: null,
}
}
handleSubmit = ({ values, setSubmitting }) => {
const { onClickBtn } = this.state
this.props.actions.createInfo(values, onClickBtn)
}
handleCancel = () => {
...
}
setOnClickBtn(name) {
this.setState({
onClickBtn: name,
})
}
render() {
return (
)
}
}
复制代码
此时Form的children是个function
,要测试表单中按钮点击事件,如果只用shallow,是无法找到Form中children的元素的,因此这里采用mount
方式将整个dom渲染,可直接模拟type为submit属性的那个button的点击事件。 然后测试点击该button是否完成了2个事件:handleSubmit
和setOnclickBtn
。
有人会想到模拟form的submit
事件,但在mount的情况下,模拟button的click
事件同样可以触发onSubmit事件。
由于submit过程要涉及子控件的交互,其过程具有一定的不确定性,此时需要设置一个timeout
,延长一段时间再来判断submit内的action是否被执行。
it('should call create role action when click save', () => {
const preProps = {
actions: {
createInfo: jest.fn(),
}
}
const { props, enzymeWrapper } = setup(preProps)
const nameInput = enzymeWrapper.find('input').at(0)
nameInput.simulate('change', { target: { value: 'RoleName' } })
const keyInput = enzymeWrapper.find('input').at(1)
keyInput.simulate('change', { target: { value: 'RoleKey' } })
const saveButton = enzymeWrapper.find('button[type="submit"]').at(0)
saveButton.simulate('click')
expect(enzymeWrapper.state().onClickBtn).toBe('save')
setTimeout(() => {
expect(props.actions.createInfo).toHaveBeenCalled()
}, 500)
})
复制代码
但是用mount
来渲染也有容易让人失误的地方,比如说要找到子组件,可能需要多层.children()
才能找到。在单元测试中,应尽量采用shallow
渲染,测试粒度尽可能减小。
含有Promise的情况
有的组件的函数逻辑中会含有Promise
,其返回结果带有不确定性,例如以下代码段中的auth.handleAuthenticateResponse
,传入的参数是一个callback函数,需要根据auth.handleAuthenticateResponse
的处理结果是error
还是正常的result
来处理自己的内部逻辑。
handleAuthentication = () => {
const { location, auth } = this.props
if (/access_token|id_token|error/.test(location.search)) {
auth.handleAuthenticateResponse(this.handleResponse)
}
}
handleResponse = (error, result) => {
const { auth } = this.props
let postMessageBody = null
if (error) {
postMessageBody = error
} else {
auth.setSession(result)
postMessageBody = result
}
this.handleLogicWithState(postMessageBody)
}
复制代码
在测试时,可用jest.fn()模拟出auth.handleAuthenticateResponse
函数,同时让它返回一个确定的结果。
const preProps = {
auth: {
handleAuthenticateResponse: jest.fn(cb => cb(errorMsg))
}
}
setup(preProps)
复制代码
相关API
enzyme: airbnb.io/enzyme/
Jest: jestjs.io/docs/en/api
自留问题
- 使用mount测试一个包含子组件的父组件,以及父子组件的交互过程时,这种测试叫做UT测试还是CT组件测试?
- 组件snapshot测试是什么?有无必要?如何测?