最近在项目中大量使用React Testing Library来写测试,踩了不少坑,也积累了一些经验,今天就来总结一下React Testing Library的最佳实践,希望能给正在学习或者使用这个测试库的朋友一些参考和帮助。
React Testing Library是Kent C. Dodds创建的一个React组件测试库,它的核心理念是,测试应该关注用户的行为,而不是组件的实现细节,这样的测试更可靠,更有价值,也更容易维护。相比Enzyme,React Testing Library更轻量,更简单,也更符合现代前端测试的理念,越来越多的项目都在从Enzyme迁移到React Testing Library。
今天这篇文章,就来详细聊聊React Testing Library的最佳实践。
一、先说说为什么用React Testing Library
先简单说说,为什么,我们,选择,React Testing Library,而,不是,Enzyme,或者,其他,测试,库。
之前,我们,项目,用的,是,Enzyme,来,写,React,组件,测试,Enzyme,功能,很,强大,支持,shallow,mount,render,三种,渲染,方式,也,支持,直接,访问,组件,的,state,props,实例,方法,等等,写,测试,很,灵活。
但是,用,了,一段时间,之后,我们,发现,Enzyme,写,的,测试,有,很多,问题:
- 测试容易耦合实现细节:Enzyme,允许,直接,访问,组件,的,state,props,实例,方法,很多人,写,测试,的,时候,会,直接,断言,state,是,什么,或者,直接,调用,实例,方法,这样,的,测试,和,组件,的,实现,细节,耦合,太,紧,组件,重构,的,时候,比如,改,了,state,的,名字,或者,把,类,组件,改成,函数,组件,测试,就,会,失败,但是,组件,的,功能,其实,是,好的,这样,的,测试,很,脆弱,维护,成本,很高。
- shallow渲染问题多:Enzyme,的,shallow,渲染,只,渲染,当前,组件,不,渲染,子,组件,这样,虽然,测试,速度,快,但是,很多,问题,发现,不了,比如,子,组件,的,props,传,错,了,子,组件,的,事件,处理,有,问题,等等,shallow,渲染,都,发现,不了,测试,通过,了,但是,实际,运行,的,时候,却,有,问题。
- 测试不像用户行为:Enzyme,写,的,测试,很多,是,从,开发者,的,角度,写,的,比如,找到,某个,组件,调用,某个,方法,断言,某个,state,变化,了,但是,用户,根本,不,关心,这些,用户,关心,的,是,界面,上,显示,了,什么,点击,某个,按钮,之后,发生,了,什么,所以,Enzyme,写,的,测试,很多,不能,真正,反映,用户,的,使用,场景,测试,价值,有限。
后来,我们,尝试,了,React Testing Library,发现,它,的,理念,很,好,解决,了,Enzyme,的,很多,问题:
- 关注用户行为,不耦合实现细节:React Testing Library,的,核心理念,就是,测试,应该,像,用户,一样,去,使用,组件,查询,元素,的,方式,是,用户,能,看到,的,比如,文本,label,placeholder,role,等等,而,不是,CSS,选择器,或者,组件,的,state,props,这样,测试,和,实现,细节,解耦,组件,重构,的,时候,只要,功能,不变,测试,就,不会,失败,测试,更,可靠,更,容易,维护。
- 默认完整渲染,更接近真实场景:React Testing Library,默认,是,完整,渲染,组件,包括,子,组件,这样,测试,更,接近,真实,的,使用,场景,能,发现,更多,问题,比如,子,组件,的,props,传,错,了,事件,处理,有,问题,等等,都,能,发现,测试,价值,更高。
- API简单,学习成本低:React Testing Library,的,API,很,简单,核心,就是,查询,元素,的,API,和,模拟,事件,的,API,没有,那么,多,概念,和,方法,学习,成本,很低,上手,很快。
- 社区活跃,生态好:React Testing Library,的,社区,很,活跃,维护,很,积极,文档,也,很,完善,而且,Create React App,已经,默认,集成,了,React Testing Library,说明,它,已经,成为,了,React,测试,的,主流,选择。
就,这样,我们,把,项目,的,测试,逐步,从,Enzyme,迁移,到,了,React Testing Library,用,了,一段时间,感觉,很,好,测试,更,可靠,了,维护,成本,也,降低,了,今天,就,来,分享,一下,我们,总结,的,最佳,实践。
二、环境搭建
先说说,环境,搭建,如果你,用,的,是,Create React App,那么,已经,默认,集成,了,React Testing Library,不用,额外,配置,直接,就能,用。
如果,你,是,自己,配置,的,项目,那么,需要,安装,以下,依赖:
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event@testing-library/react:核心,库,提供,渲染,组件,和,查询,元素,的,API。@testing-library/jest-dom:提供,一些,常用的,Jest,断言,比如,toBeInTheDocument,toHaveTextContent,等等,让,断言,更,语义化。@testing-library/user-event:提供,更,真实,的,用户,事件,模拟,比如,click,type,selectOptions,等等,比,内置,的,fireEvent,更,接近,真实,用户,行为。
然后,在,测试,文件,的,开头,引入,@testing-library/jest-dom,这样,就能,用,那些,语义化,的,断言,了:
import '@testing-library/jest-dom';或者,在,Jest,的,配置,文件,里,配置,setupFilesAfterEnv,这样,每个,测试,文件,都,自动,引入,不用,每个,文件,都,写,一遍:
// jest.config.js
module.exports = {
setupFilesAfterEnv: ['@testing-library/jest-dom'],
};环境,搭建,好,之后,就,能,开始,写,测试,了。
三、查询元素的最佳实践
查询,元素,是,写,测试,最,常用,的,操作,React Testing Library,提供,了,很多,查询,元素,的,API,比如,getByText,getByRole,getByLabelText,getByPlaceholderText,getByTestId,等等,那么,该,怎么,选择,呢?
1. 优先使用用户能感知的查询方式
React Testing Library,的,理念,是,测试,应该,像,用户,一样,去,使用,组件,所以,查询,元素,的,时候,应该,优先,使用,用户,能,感知,的,查询,方式,比如:
getByRole:通过,元素,的,ARIA role,查询,这是,最,推荐,的,查询,方式,因为,它,能,同时,检查,元素,是否,可访问,比如,按钮,的,role,是,button,输入框,的,role,是,textbox,等等,用户,通过,辅助,技术,也,能,感知,到,这些,role。getByLabelText:通过,表单,元素,的,label,文本,查询,这是,查询,表单,输入框,的,最佳,方式,因为,用户,就是,通过,label,来,找到,输入框,的。getByText:通过,元素,的,文本,内容,查询,这是,查询,按钮,链接,标题,等,元素,的,常用,方式,用户,也是,通过,文本,来,找到,这些,元素,的。getByPlaceholderText:通过,输入框,的,placeholder,查询,当,输入框,没有,label,的,时候,可以,用,这个。getByAltText:通过,图片,的,alt,属性,查询,这是,查询,图片,的,最佳,方式,因为,alt,属性,是,给,用户,看,的。getByTitle:通过,元素,的,title,属性,查询。
这些,查询,方式,都是,用户,能,感知,的,优先,使用,这些,能,让,测试,更,接近,真实,用户,行为,也,能,顺便,检查,组件,的,可访问性。
2. 尽量避免使用getByTestId
getByTestId,是,通过,元素,的,data-testid,属性,查询,这个,方式,很,灵活,但是,不,推荐,优先,使用,因为,data-testid,是,专门,给,测试,加,的,属性,用户,感知,不到,如果,大量,使用,getByTestId,测试,就,和,实现,细节,耦合,了,而且,也,不能,检查,可访问性。
但是,在,某些,场景,下,getByTestId,还是,有用,的,比如,元素,没有,文本,没有,role,没有,label,很难,用,其他,方式,查询,比如,动态,生成,的,图表,自定义,的,复杂,组件,等等,这,时候,可以,用,getByTestId,但是,要,尽量,少用。
3. getBy、queryBy、findBy的区别
React Testing Library,的,查询,API,有,三种,前缀,getBy,queryBy,findBy,它们,的,区别,是:
getBy:查询,元素,如果,找,不到,会,抛出,错误,适合,断言,元素,一定,存在,的,场景。queryBy:查询,元素,如果,找,不到,会,返回,null,不会,抛出,错误,适合,断言,元素,不存在,的,场景,比如,expect(queryByText('提交')).toBeNull()。findBy:异步,查询,元素,返回,一个,Promise,会,等待,元素,出现,如果,超时,还,没,出现,会,抛出,错误,适合,异步,渲染,的,场景,比如,组件,加载,数据,之后,才,显示,某个,元素。
要,根据,场景,选择,合适,的,前缀,不要,都,用,getBy,比如,断言,元素,不存在,的,时候,用,getBy,会,直接,报错,测试,通不过,这,时候,应该,用,queryBy。
4. 多个元素用getAllBy
如果,页面,上,有,多个,匹配,的,元素,用,getBy,会,报错,因为,它,期望,只有,一个,元素,这,时候,应该,用,getAllBy,返回,元素,数组,然后,可以,断言,数组,的,长度,或者,操作,某个,元素。
同样,queryAllBy,和,findAllBy,也,是,对应,的,多个,元素,的,版本。
四、事件模拟的最佳实践
模拟,用户,事件,也是,写,测试,常用,的,操作,React Testing Library,提供,了,fireEvent,来,模拟,事件,另外,@testing-library/user-event,提供,了,更,真实,的,userEvent。
1. 优先使用userEvent,而不是fireEvent
fireEvent,是,React Testing Library,内置,的,事件,模拟,API,它,直接,触发,DOM,事件,比如,fireEvent.click(button),直接,触发,按钮,的,click,事件。
但是,fireEvent,触发,的,事件,不够,真实,比如,用户,点击,输入框,的,时候,会,先,触发,mouseDown,mouseUp,然后,才,触发,click,而且,输入框,会,获得,焦点,但是,fireEvent.click,只,触发,click,事件,不会,触发,前面,的,事件,也,不会,让,输入框,获得,焦点,所以,不够,真实。
userEvent,是,@testing-library/user-event,提供,的,更,真实,的,事件,模拟,它,会,模拟,用户,真实,的,操作,序列,比如,userEvent.click,会,先,触发,mouseOver,mouseMove,mouseDown,mouseUp,click,等等,更,接近,真实,用户,行为,也,能,发现,更多,问题。
所以,推荐,优先,使用,userEvent,而,不是,fireEvent,比如:
import userEvent from '@testing-library/user-event';
// 点击按钮
userEvent.click(screen.getByText('提交'));
// 输入文本
userEvent.type(screen.getByLabelText('用户名'), '张三');
// 选择下拉框
userEvent.selectOptions(screen.getByLabelText('城市'), ['北京']);2. 输入文本用type,不要用fireEvent.change
模拟,用户,输入,文本,的,时候,不要,用,fireEvent.change,直接,设置,输入框,的,值,因为,这,不,真实,用户,输入,是,一个,字符,一个,字符,输入,的,会,触发,keyDown,keyPress,input,keyUp,等等,事件,而且,输入,过程,中,输入框,是,获得,焦点,的。
应该,用,userEvent.type,它,会,模拟,用户,一个,字符,一个,字符,输入,更,真实,也,能,触发,所有,相关,的,事件,比如:
userEvent.type(screen.getByLabelText('用户名'), '张三');如果,需要,清空,输入框,再,输入,可以,用,userEvent.clear,先,清空,再,输入:
const input = screen.getByLabelText('用户名');
userEvent.clear(input);
userEvent.type(input, '张三');3. 异步操作之后要等待
如果,点击,按钮,之后,组件,会,异步,更新,比如,发送,请求,然后,更新,状态,那么,不要,直接,断言,因为,异步,操作,还,没,完成,断言,会,失败。
应该,用,findBy,或者,waitFor,来,等待,异步,操作,完成,再,断言,比如:
// 点击提交按钮
userEvent.click(screen.getByText('提交'));
// 等待成功提示出现
const successMessage = await screen.findByText('提交成功');
expect(successMessage).toBeInTheDocument();或者,用,waitFor:
userEvent.click(screen.getByText('提交'));
await waitFor(() => {
expect(screen.getByText('提交成功')).toBeInTheDocument();
});五、异步测试的最佳实践
异步,测试,是,前端,测试,的,重点,也是,难点,因为,前端,很多,操作,都是,异步,的,比如,加载,数据,发送,请求,动画,等等,下面,说说,异步,测试,的,最佳,实践。
1. 用findBy查询异步渲染的元素
如果,元素,是,异步,渲染,的,比如,组件,加载,数据,之后,才,显示,某个,元素,那么,不要,用,getBy,因为,getBy,是,同步,的,元素,还,没,渲染,出来,会,报错。
应该,用,findBy,它,是,异步,的,会,等待,元素,出现,默认,超时,是,1000ms,如果,需要,更,长,时间,可以,配置,超时,时间,比如:
// 等待元素出现,默认超时1000ms
const element = await screen.findByText('加载完成');
// 配置超时时间为5000ms
const element = await screen.findByText('加载完成', {}, { timeout: 5000 });2. 用waitFor等待异步条件
如果,需要,等待,某个,条件,成立,而,不是,简单,的,元素,出现,那么,可以,用,waitFor,它,会,不断,重试,直到,回调,函数,不,抛出,错误,或者,超时,比如:
await waitFor(() => {
expect(mockApi.fetchData).toHaveBeenCalledTimes(1);
});waitFor,默认,超时,是,1000ms,也,可以,配置,超时,时间,和,重试,间隔,比如:
await waitFor(() => {
expect(screen.getByText('加载完成')).toBeInTheDocument();
}, { timeout: 5000, interval: 100 });3. 用findByText而不是getByText加setTimeout
很多,新手,写,异步,测试,的,时候,会,用,setTimeout,来,等待,异步,操作,完成,比如:
// 错误的写法
test('异步测试', () => {
render(<MyComponent />);
setTimeout(() => {
expect(screen.getByText('加载完成')).toBeInTheDocument();
}, 1000);
});这样,的,写法,是,错误的,因为,Jest,不会,等待,setTimeout,里,的,断言,测试,会,直接,通过,就算,断言,失败,了,也,不会,报错。
正确,的,写法,是,用,async/await,加,findBy,或者,waitFor,比如:
// 正确的写法
test('异步测试', async () => {
render(<MyComponent />);
const element = await screen.findByText('加载完成');
expect(element).toBeInTheDocument();
});4. Mock异步请求
测试,组件,的,时候,一般,不,应该,真正,发送,网络,请求,因为,网络,请求,慢,而且,不稳定,会,影响,测试,的,速度,和,可靠性,应该,Mock,异步,请求,返回,固定,的,数据。
可以,用,jest.mock,来,Mock,API,模块,比如:
import { fetchData } from './api';
jest.mock('./api');
test('加载数据', async () => {
fetchData.mockResolvedValue({ name: '张三' });
render(<MyComponent />);
expect(await screen.findByText('张三')).toBeInTheDocument();
});这样,测试,的,时候,就,不会,真正,发送,请求,而是,返回,我们,Mock,的,数据,测试,更,快,也,更,稳定。
六、Mock的最佳实践
Mock,是,前端,测试,常用,的,技术,用来,隔离,依赖,让,测试,更,快,更,稳定,下面,说说,Mock,的,最佳,实践。
1. 只Mock需要Mock的部分
Mock,不是,越多,越好,应该,只,Mock,需要,Mock,的,部分,比如,网络,请求,定时器,等等,不要,什么,都,Mock,不然,测试,就,失去,了,意义。
比如,测试,组件,的,时候,组件,内部,的,工具,函数,就,不用,Mock,让,它,真正,运行,这样,能,发现,更多,问题,测试,更,有,价值。
2. Mock要在测试之间重置
如果,用,jest.mock,Mock,了,模块,那么,Mock,的,函数,的,调用,记录,和,返回值,会,在,测试,之间,保留,可能,会,影响,其他,测试,所以,应该,在,每个,测试,之前,重置,Mock。
可以,用,beforeEach,加,jest.clearAllMocks(),来,重置,所有,Mock,的,调用,记录,和,返回值,比如:
beforeEach(() => {
jest.clearAllMocks();
});或者,用,jest.resetAllMocks(),不仅,重置,调用,记录,和,返回值,还,重置,Mock,的,实现,根据,需要,选择。
3. 不要Mock测试对象本身
不要,Mock,你,正在,测试,的,对象,本身,比如,测试,某个,组件,就,不要,Mock,这个,组件,内部,的,方法,因为,这样,测试,就,没有,意义,了,你,测试,的,只是,你,Mock,的,东西,而,不是,真实,的,组件。
应该,从,外部,测试,组件,的,行为,比如,传入,props,模拟,用户,事件,断言,界面,的,变化,而,不是,Mock,组件,内部,的,方法,断言,方法,被,调用,了。
七、常见错误和避坑指南
最后,说说,常见,的,错误,和,避坑,指南。
1. 不要用wrapper.debug()来调试
很多人,调试,测试,的,时候,喜欢,用,screen.debug(),或者,container.innerHTML,来,打印,DOM,结构,这,当然,可以,但是,不要,过度,依赖,因为,打印,出来,的,DOM,可能,和,真实,的,不一样,而且,很,长,不,容易,看。
更好,的,调试,方式,是,用,testing-library/playground,它,能,把,当前,的,DOM,保存,成,一个,HTML,文件,然后,在,浏览器,里,打开,能,真实,地,看到,页面,的,样子,也,能,用,开发者,工具,调试,很,方便。
2. 不要在测试里用act警告
如果你,看到,测试,有,act(),的,警告,说明,你,的,测试,有,问题,组件,的,状态,更新,没有,被,包裹,在,act(),里。
React Testing Library,的,API,比如,render,fireEvent,userEvent,findBy,waitFor,等等,已经,内部,包裹,了,act(),所以,一般,不用,手动,写,act()。
如果,出现,了,act(),警告,通常,是,因为,你,在,测试,里,直接,调用,了,会,更新,组件,状态,的,函数,而,没有,用,React Testing Library,的,API,或者,异步,操作,没有,等待,完成,检查,一下,测试,代码,修正,就,好,了。
3. 不要测试实现细节
这是,最,重要,的,一点,不要,测试,实现,细节,比如,不要,断言,组件,的,state,是,什么,不要,断言,组件,的,某个,内部,方法,被,调用,了,不要,用,CSS,类名,或者,组件,名,查询,元素。
应该,测试,组件,的,行为,从,用户,的,角度,测试,比如,传入,props,模拟,用户,事件,断言,界面,上,显示,了,什么,这样,的,测试,才,有,价值,也,容易,维护。
记住,React Testing Library,的,核心理念,就是,测试,应该,关注,用户,的,行为,而,不是,组件,的,实现,细节,时刻,记住,这,一点,就能,写出,好,的,测试。
八、写在最后
React Testing Library,是,一个,很,好,的,React,组件,测试,库,它,的,理念,很,先进,API,很,简单,能,帮,我们,写出,更,可靠,更,有,价值,更,容易,维护,的,测试。
但是,工具,只是,工具,更,重要,的,是,理念,要,记住,测试,应该,关注,用户,的,行为,而,不是,组件,的,实现,细节,从,用户,的,角度,写,测试,这样,的,测试,才,有,意义。
希望,这篇,文章,总结,的,最佳,实践,能,帮,到,大家,让,大家,写出,更好,的,测试,提升,代码,质量,减少,bug。
也,欢迎,大家,在,评论,区,分享,你们,的,经验,和,问题,一起,交流,一起,进步。
愿,我们,都,能,写出,高质量,的,代码,和,高质量,的,测试,让,我们,的,产品,更,稳定,更,可靠。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录