Open closertb opened 4 years ago
写于:2018-09-28
关于前端React组件测试(jest,Enzyme),网上有大量的入门文章,可以看看,但如果你确实想了解前端自动化测试,个人更推荐看官方的文档和一些比较官方的测试案列,这里推荐两个:
组件测试最重要的前提,就是你需要知道怎么实例化自己的组件,然后才能去判断是否渲染正常,交互怎么样,功能是不是都OK,而这就和下面要说的渲染方法相关,了解一下三种方法的区别,有助于自己少掉坑,少抠脑壳,少掉头发,用一个关于antd Select组件的示例来说明。
function RenderTest() { return ( <div className="render"> <Select> <Option key="1" value="1">test1</Option> <Option key="2" value="2">test2</Option> <Option key="3" value="3">test3</Option> </Select> </div> ); } describe('base render test', () => { it('component mounted right', () => { const wrapper = mount( <RenderTest /> ); console.log('***mount***', 'Select:', wrapper.find('Select').length, ' Option:', wrapper.find('Option').length, ' Div', wrapper.find('div').length, ' class', wrapper.find('.render').length); }); });
在浏览器中,挂载这个RenderTest,渲染出来的DomTree是这样的: 然后在组件测试中分别用了三种方法来渲染,得到是下面的结果:
下面两节都将以自己最近封装的一个Antd组件为例来做说明,在我前面的一篇文章里有提到,如下图所示:
这个组件的大致功能如上面图所示,产品需求就是需要一个编辑框,这个框在用户点击输入时,需要弹出一个搜索框,根据用户的输入远程搜索获取数据形成一个下拉列表,供用户选择,选择完成后搜索框被收起。而从代码上,也很简单:
<div className="originSearch" style={style} ref={el => this.searchInputWrapper = el} > <Input ref={e => this.searchInput = e} readOnly placeholder={placeholder} value={valueFormat(value)} style={{ width: '100%' }} size="default" {...inputProps} /> { isShowSearch && <div className="js-origin-search origin-search"> <Icon type="search" className="origin-search-icon" /> <AutoComplete autoFocus ref={el => this.searchRealInput = el} className="certain-category-search" dropdownClassName="certain-category-search-dropdown" dropdownMatchSelectWidth onSearch={this.handleChange} onSelect={this.handleSelect} style={{ width: '100%' }} optionLabelProp="value" > {loading ? [<Option key="loading" disabled><Spin spinning={loading} style={{ paddingLeft: '45%', textAlign: 'center' }} /></Option>] : options} </AutoComplete> </div> } </div>
第一个事件,用户开始输入,Input被单击,click事件捕捉,isShowSearch由false变为true, AutoComplete组件渲染,并自动获得焦点。 对于这一个测试,这是需要有用户交互的,和测试点击浏览器,所以我们需要用到模拟事件simulate,看下面代码:
it('Input state change disable when click', () => { const wrapper = mount( <HInputSearch {...searchProps} /> ); wrapper.find('input').simulate('click'); expect(wrapper.state('isShowSearch')).toEqual(true); const inputNodes = wrapper.find('input'); expect(inputNodes.length).toEqual(2); });
除了模拟点击事件,还可以模拟输入框值的change事件,接着我们还可以检测这个变化是否触发了相应的方法,比如下面这段:
it('Input state change disable when click', () => { const inputValue = 'change'; const change = jest.spyOn(HInputSearch.prototype, 'handleChange'); // handleChange是在定义组件时,定义的一个原型方法 const wrapper = mount( <HInputSearch {...searchProps} /> ); wrapper.find('input').simulate('click'); expect(wrapper.state('isShowSearch')).toEqual(true); const inputNode = wrapper.find('.origin-search input'); inputNode.simulate('change', { target: { value: inputValue } }); expect(wrapper.find('input').get(1).props.value).toEqual(inputValue); expect(change).toBeCalledWith(inputValue); // 这里可以用toBeCalled检测是否调用,而使用toBeCalledWith除了检测是否调用,还可以检测是否正确的传参; });
除了在原型上直接mock响应的方法,也可以直接在实例上,查找出某个节点利用jest.spyOn来检测某个方法是否被调用。在下一节还会继续对jest的函数mock进行说明。
此次封装的案例组件我称之为远程搜索输入框,所以涉及到防抖与异步请求的发起,所以在Input框值变化时,首先是使用lodash的debounce函数防抖,然后发起请求。所以当我们进行触发后的流程测试时,比如异步请求是否被调用,返回值是否正常的被存入state,Option是否生成,这些统统没法立即执行测试,而是需要一段时间的等待再来判断,我们把这称之为异步测试。进行这个测试,先理一理思路:
export const response = [{ name: '李梅梅', id: 12, }, { name: '徐雷雷', id: 13, }, { name: 'james', id: 14, }]; export default function fetch() { return new Promise(resolve => setTimeout(() => { resolve(response); }, 5000) ); } const mockFetch = jest.fn(val => fetch(val)); const mockFormat = jest.fn(data => data.map(({ id, name }, index) => ({ label: `${name}(${id})`, value: name, key: index }))); const searchProps = { value: initValue, style: { width: '100%' }, search: { keyword: undefined, }, onSelect: mockSelect, format: mockFormat, // 利用jest.fn() mock的 fetchData: mockFetch, // 利用jest.fn() mock的 }; it('Input state change disable when click', (done) => { // 异步测试必备 const inputValue = 'change'; const change = jest.spyOn(HInputSearch.prototype, 'handleChange'); // handleChange是在定义组件时,定义的一个原型方法 const wrapper = mount( <HInputSearch {...searchProps} /> ); wrapper.find('input').simulate('click'); const inputNode = wrapper.find('.origin-search input'); inputNode.simulate('change', { target: { value: inputValue } }); expect(change).toBeCalledWith(inputValue); // 这里可以用toBeCalled检测是否调用,而使用toBeCalledWith除了检测是否调用,还可以检测是否正确的传参; setTimeout(() => { // 模拟异步任务 expect(mockFetch).toHaveBeenCalledWith({ keyword: inputValue }); expect(mockFormat).toBeCalled(); done(); // 这个done()很重要,会告诉这个异步测试是否完成 }, 1000); });
结合上面的实例可以看出,好像异步测试也不是很麻烦,就在测试用例中多了个测试完的回调函数done;异步请求和mock函数其实质都是用jest.fn直接包一下;异步任务可以直接用setTimeout或者setImmediate来模拟,当然也有文艺一点的写法,比如写一个通用的:
// 异步任务模拟 function mockPromises() { return new Promise(resolve => setTimeout(() => { resolve(); }, yourTime)); }
写完这一个组件,自己收获还是非常大的,并不是学了jest或者enzyme这么多API的使用,说实话,这个意义真不大。主要意义在于不自愿的去看了一些Antd组件以及Antd 底层组件的一些实现源码,它的高阶组件应用以及组件拆分的方式让我还是很有收获。另外,我其实一直在思考,写单元测试的意义,因为开始我以为这个很神奇,能够写一些逻辑什么的,就能找出自己组件的bug.其实不是,单测只是在你能想到的案列进行描述,然后看是否运行正常,有些小bug也许能找出来,但一些没想到的应用场景,bug还是在哪里,并没有被发现。所以我觉得意义不大,但后面一次优化,改变了我的想法。试想:
写于:2018-09-28
关于前端React组件测试(jest,Enzyme),网上有大量的入门文章,可以看看,但如果你确实想了解前端自动化测试,个人更推荐看官方的文档和一些比较官方的测试案列,这里推荐两个:
三种方法渲染的区别
组件测试最重要的前提,就是你需要知道怎么实例化自己的组件,然后才能去判断是否渲染正常,交互怎么样,功能是不是都OK,而这就和下面要说的渲染方法相关,了解一下三种方法的区别,有助于自己少掉坑,少抠脑壳,少掉头发,用一个关于antd Select组件的示例来说明。
在浏览器中,挂载这个RenderTest,渲染出来的DomTree是这样的: 然后在组件测试中分别用了三种方法来渲染,得到是下面的结果:
组件事件的模拟及事件回调的mock
下面两节都将以自己最近封装的一个Antd组件为例来做说明,在我前面的一篇文章里有提到,如下图所示:
这个组件的大致功能如上面图所示,产品需求就是需要一个编辑框,这个框在用户点击输入时,需要弹出一个搜索框,根据用户的输入远程搜索获取数据形成一个下拉列表,供用户选择,选择完成后搜索框被收起。而从代码上,也很简单:
第一个事件,用户开始输入,Input被单击,click事件捕捉,isShowSearch由false变为true, AutoComplete组件渲染,并自动获得焦点。 对于这一个测试,这是需要有用户交互的,和测试点击浏览器,所以我们需要用到模拟事件simulate,看下面代码:
除了模拟点击事件,还可以模拟输入框值的change事件,接着我们还可以检测这个变化是否触发了相应的方法,比如下面这段:
除了在原型上直接mock响应的方法,也可以直接在实例上,查找出某个节点利用jest.spyOn来检测某个方法是否被调用。在下一节还会继续对jest的函数mock进行说明。
异步请求的模拟
此次封装的案例组件我称之为远程搜索输入框,所以涉及到防抖与异步请求的发起,所以在Input框值变化时,首先是使用lodash的debounce函数防抖,然后发起请求。所以当我们进行触发后的流程测试时,比如异步请求是否被调用,返回值是否正常的被存入state,Option是否生成,这些统统没法立即执行测试,而是需要一段时间的等待再来判断,我们把这称之为异步测试。进行这个测试,先理一理思路:
结合上面的实例可以看出,好像异步测试也不是很麻烦,就在测试用例中多了个测试完的回调函数done;异步请求和mock函数其实质都是用jest.fn直接包一下;异步任务可以直接用setTimeout或者setImmediate来模拟,当然也有文艺一点的写法,比如写一个通用的:
结语
写完这一个组件,自己收获还是非常大的,并不是学了jest或者enzyme这么多API的使用,说实话,这个意义真不大。主要意义在于不自愿的去看了一些Antd组件以及Antd 底层组件的一些实现源码,它的高阶组件应用以及组件拆分的方式让我还是很有收获。另外,我其实一直在思考,写单元测试的意义,因为开始我以为这个很神奇,能够写一些逻辑什么的,就能找出自己组件的bug.其实不是,单测只是在你能想到的案列进行描述,然后看是否运行正常,有些小bug也许能找出来,但一些没想到的应用场景,bug还是在哪里,并没有被发现。所以我觉得意义不大,但后面一次优化,改变了我的想法。试想: