关于前端React组件测试(jest,Enzyme),网上有大量的入门文章,可以看看,但如果你确实想了解前端自动化测试,个人更推荐看官方的文档和一些比较官方的测试案列,这里推荐两个:
- enzyme官方文档,涵盖了各种说明和API;
- jest官方文档,涵盖了各种说明和API;
- antd基础组件库,每个组件都有较丰富的测试用例;
本篇文章适合对前端组件测试有一定概念的同学,本文将包含以下几点:
- shallow, mount, render三种方法渲染的区别;
- 组件事件的模拟及事件回调的mock;
- 异步事件的模拟;
三种方法渲染的区别
组件测试最重要的前提,就是你需要知道怎么实例化自己的组件,然后才能去判断是否渲染正常,交互怎么样,功能是不是都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是这样的:
然后在组件测试中分别用了三种方法来渲染,得到是下面的结果:
- Shallow:与语义一样,肤浅表面的,也是浅渲染, 大概就是组件长啥样,就渲染成啥样,子组件不会递归渲染;
- Mount: 又称Full DOM rendering,组件层虚拟Dom与真实Dom的渲染,所以这个方法里你既能看到Select节点(为2与组件的定义有关),Option为0,因为Antd的Option都是以绝对定位的方式挂载在body节点下的,而非wrapper节点里。你还可以打印wrapper.find('Trigger')的长度,结果为1,至于为什么,和Select为2一样,需要去看Select的源码;
- Render: 又称静态渲染,真实dom节点的渲染,无虚拟Dom,不过有趣的是wrapper节点就是根节点,所以wrapper.find('.render')的length为0,而且很多前两者能用的方法在这里都没有,比如containsMatchingElement这样最基本的方法。
组件事件的模拟及事件回调的mock
下面两节都将以自己最近封装的一个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是否生成,这些统统没法立即执行测试,而是需要一段时间的等待再来判断,我们把这称之为异步测试。进行这个测试,先理一理思路:
- 首先: 需要模拟一个异步请求;
- 其次: 需要模拟获取数据后数据转换函数format;
- 最后: 模拟一个异步任务,这个简单,用setTimeout就可以。
来看一下实现:
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还是在哪里,并没有被发现。所以我觉得意义不大,但后面一次优化,改变了我的想法。试想:
- 当你歇了一段时间,也许你自己写的组件你都看不懂了,但你接到团队其他人的需求需要优化其中的一部分代码,你改完觉得没问题,就push了代码,但是一上线,发现动了不改动的逻辑,影响了最初的功能。这个时候,单元测试的意义就体现了。你第一次写了这个组件,并且写了相应的单元测试,并且代码测试覆盖率能达到80%,当你下一次优化时,优化完你只需要再跑一遍以前写的单元测试(如果是功能性的优化,有必要针对这个补充一个针对性的单元测试),如果测试跑通,你在push代码,这样发生bug的概率会显著下降。
以上就是本篇文章所有,谢谢浏览。有什么描述不严谨之处,还请指正。
原文见:地址
源码及测试用例:地址
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。