前言
测试是应用生产过程中不可缺少的一个环节,开发人员在编码时总有考虑不周全或者出错的情况,而测试则是通过对比实际结果与预期结果来找出问题和缺陷,从而确保软件的质量。本文主要介绍了在最近在工作中用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(<Dialog {...props} />) 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 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的情况
有的组件的函数逻辑中会含有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) |
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。