测试是应用生产过程当中不可缺乏的一个环节,开发人员在编码时总有考虑不周全或者出错的状况,而测试则是经过对比实际结果与预期结果来找出问题和缺陷,从而确保软件的质量。本文主要介绍了在最近在工做中用Jest
和Enzyme
来测试React 组件的过程和容易踩坑的地方。api
对于一个Web网站来讲,测试的种类主要分为如下3种:浏览器
市面上如今有不少测试工具,公司里采用Umijs做为脚手架快速搭建了一个React应用,而Umi内部采用了Dva做为数据流管理,同时也自动配置了Jest测试框架。bash
Jest
测试框架由Facebook所推荐,其优势是运行快性能好,而且涵盖了测试所需的多种功能模块(包括断言,模拟函数,比较组件的快照snapshot,运行测试,生成测试结果和覆盖率等),配置简单方便,适合大项目的快速测试。app
测试React组件咱们采用Enzyme
工具库,它提供3种组件渲染方式:框架
Shallow
:不会渲染子组件Mount
: 渲染子组件,同时包含生命周期函数如componentDidMount
Render
: 渲染子组件,但不会包含生命周期,同时可用的API也会减小好比setState()
通常状况下用shallow和mount的状况比较多。dom
有些组件被Connect
包裹起来,这种状况不能直接测,须要创建一个Provider
和传入一个store
,这种过程比较痛苦,最好是将去掉Connect
后的组件 export
出来单独测,采用shallow
的渲染方法,仅测该组件的逻辑。ide
例如被测的组件以下:函数
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(<Dialog {...props} />)
return {
props,
enzymeWrapper,
}
}
复制代码
有的组件,须要测试和原生DOM元素的交互,好比要测点击原生button元素,是否触发当前的组件的事件,或者须要测试和子组件的交互时,这时候用须要用mount来渲染。post
例如,个人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 onSubmit={this.handleSubmit}>
{({ handleChange }) => {
return (
<div className="information-form">
<Input name={FIELD_ROLE_NAME} onChange={handleChange}
/>
<Input name={FIELD_ROLE_KEY} onChange={handleChange}
/>
<div>
<Button type="button" onClick={this.handleCancel}> Cancel </Button>
<Button type="submit" primary onClick={() => this.setOnClickBtn('create')} > Create </Button>
<Button type="submit" primary onClick={() => this.setOnClickBtn('continue')} > Create and Continue </Button>}
</div>
</div>
)
}}
</Form>
)
}
}
复制代码
此时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
,其返回结果带有不肯定性,例如如下代码段中的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)
复制代码
enzyme: airbnb.io/enzyme/
Jest: jestjs.io/docs/en/api