Jest测试框架入门之快照测试附踩坑指南
Jest测试框架入门之快照测试(附踩坑指南)
文章目录
一、快照测试简介
快照测试是用于确保某个组件的UI不会有意外的改变,与UI测试不同,快照测试不会对比样式文件,仅对比dom结构和节点参数。
进行快照测试最简单的做法需要引入渲染器 react-test-renderer :
yarn add -D react-test-renderer
接下来我们写一个时间组件,使用的框架为umi3:
// MyDate/index.tsx
import { useState } from 'react';
import styles from './index.less';
const MyDate = () => {
const [time, setTime] = useState(new Date().toLocaleTimeString())
const updateTime = () => {
setTime(new Date().toLocaleTimeString())
}
return (
<div className={styles.root}>
<div className='time'>当前时间为:{time}</div>
<button className='btn' onClick={updateTime}>更新时间</button>
</div>
)
}
export default MyDate
// MyDate/index.less
.root {
display: flex;
flex-direction: column;
align-items: center;
:global {
.time {
font-size: 30px;
margin-bottom: 20px;
}
}
}
写一个测试文件 MyDate.spec.tsx ,注意命名以 tsx 结尾 :
import renderer from 'react-test-renderer';
import MyDate from '../MyDate';
test('测试 MyDate 组件', () => {
const tree = renderer.create(<MyDate />).toJSON();
expect(tree).toMatchSnapshot();
})
运行结果:
遇到了很常见的错误,解析不了 less 模块。解决办法:
根据官网的方法安装并配置 identity-obj-proxy 之后,重新运行:
这种 Cannot read property ‘xxx’ of undefined 的错,大部分原因都是安装的 react 版本和 react-test-renderer 版本不匹配导致,我项目里用的 react 版本为 17,而 react-test-renderer 版本为 18,现在降级为 17:
yarn upgrade react-test-renderer@17.0.0
再次运行,报错消失,用例通过,且test目录下生成一个名为snapshots的文件夹,该文件夹下有一个快照文件:
// MyDate.spec.tsx.snap
exports[`测试 MyDate 组件 1`] = `
<div
className="root"
>
<div
className="time"
>
当前时间为:
下午5:54:02
</div>
<button
className="btn"
onClick={[Function]}
>
更新时间
</button>
</div>
`;
再次运行:
这次不是报错,是用例失败了,这是由于我们每次运行的时候取得都是当前时间,和之前保存的快照对不上,这个时候我们可以 通过 mock useState 来解决 ,修改测试文件如下:
import React from 'react';
import renderer from 'react-test-renderer';
import MyDate from '../MyDate';
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
const useStateMock: any = (initState: any) => ['中午 12:00:00', setState];
useStateSpy.mockImplementation(useStateMock);
test('测试 MyDate 组件', () => {
const tree = renderer.create(<MyDate />).toJSON();
expect(tree).toMatchSnapshot();
})
更新一下 snap 文件:
jest -u --testNamePattern='测试 MyDate 组件'
现在运行用例会通过,且 snap 文件变为:
exports[`测试 MyDate 组件 1`] = `
<div
className="root"
>
<div
className="time"
>
当前时间为:
中午12:00:00
</div>
<button
className="btn"
onClick={[Function]}
>
更新时间
</button>
</div>
`;
二、Enzyme
如果需要进行组件行为监测,当 React 版本 <= 16 时可以使用 Enzyme 库。首先安装一些依赖:
yarn add -D enzyme @types/enzyme @wojtekmaj/enzyme-adapter-react-17
上面安装的@wojtekmaj/enzyme-adapter-react-17 是非官方版的适配器,这也是为什么 不推荐 React 版本 >= 17 时使用 enzyme。
我们写一个非常简单的 Counter 组件:
import { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
const handleReset = () => {
setCount(0)
}
return (
<div>
<button onClick={handleClick}>该按钮点击了{count}次</button>
<button onClick={handleReset}>重置点击次数</button>
</div>
)
}
export default Counter
写一个测试文件:
import Counter from '../Counter';
import { configure, shallow } from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
configure({ adapter: new Adapter() })
test('测试 Counter 组件', () => {
const c = shallow(<Counter />)
expect(c.find('button').at(0).text()).toBe('该按钮点击了 0 次')
c.find('button').at(0).simulate('click')
c.find('button').at(0).simulate('click')
expect(c.find('button').at(0).text()).toBe('该按钮点击了 2 次')
c.find('button').at(1).simulate('click')
expect(c.find('button').at(0).text()).toBe('该按钮点击了 0 次')
})
运行,通过!
三、react-testing-library(推荐)
React 官网写了这么一句话: 我们推荐使用 ,它使得针对组件编写测试用例就像终端用户在使用它一样方便。
还有一篇文章也值得一看:
从 npm 的周下载量上可以看到 react-testing-library 是 enzyme 的两倍有余,看来 enzyme 的时代确实已经落幕了。下面我们也简单看一下 react-testing-library 的使用:
yarn add --dev @testing-library/react
我们还是以上面的 Counter 组件为例,重写一下测试文件:
import Counter from '../Counter';
import { render, screen } from '@testing-library/react';
test('测试 Counter 组件', () => {
render(<Counter />)
screen.debug()
})
运行一下,可能会出现以下报错:
1、 Cannot find module ‘xxx’ from ‘xxx’
解决办法:将@testing-library/react 降级到 12.x.x 版本
2、 The error below may be caused by using the wrong test environment. Consider using the “jsdom” test environment.
解决办法:将 jest.config.js 中的 testEnvironment 改为 jsdom。
解决报错后,运行可看到通过 screen.debug()在控制台打印出了 dom 结构。
写一个与上面类似的测试如下:
import Counter from '../Counter';
import { render, screen, fireEvent } from '@testing-library/react';
test('测试 Counter 组件', () => {
render(<Counter />)
// screen.debug()
expect(screen.getAllByRole('button')[0].textContent).toBe('该按钮点击了 0 次');
fireEvent.click(screen.getAllByRole('button')[0])
expect(screen.getAllByRole('button')[0].textContent).toBe('该按钮点击了 1 次');
fireEvent.click(screen.getAllByRole('button')[1])
expect(screen.getAllByRole('button')[0].textContent).toBe('该按钮点击了 0 次');
})
运行,通过!
四、RTL 的最佳实践
上面用 RTL 测试框架写了一个简单的测试用例,实际在项目里使用的话建议安装以下几个依赖,以便写出更规范、更高效的测试用例:
1、 ,该库拓展了一些 jest 匹配器,可以使测试用例更具声明性且更易于阅读和维护。
yarn add -D @testing-library/jest-dom
2、 ,该库是 fireEvent 的替代品,更接近用户的真实交互场景,尽可能用 userEvent 而不是 fireEvent。
yarn add -D @testing-library/user-event
3、 和 ,这两个 eslint 插件会帮助我们在使用测试库编写测试时遵循最佳实践并预测常见错误。
yarn add -D eslint-plugin-testing-library eslint-plugin-jest-dom
OK,安装完毕之后,重新写一个测试页面:
import { useRef } from 'react';
/**
- 这里使用的是 antd-mobile v2.3.1 版本
*/
import { Toast } from 'antd-mobile';
import styles from './index.less';
const Page = () => {
const input = useRef<HTMLInputElement>(null)
const submit = () => {
if(input.current?.value.trim() !== '') {
Toast.show('提交成功', 3)
}else {
Toast.show('姓名不能为空', 3)
}
}
return (
<div className={styles.page}>
<div className='container'>
<div>姓名:</div>
<input ref={input} type="text" />
</div>
<button onClick={submit}>确认</button>
</div>
);
}
export default Page
测试文件如下:
import Page from '../index';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
jest.useFakeTimers()
test('测试 Page', () => {
render(<Page />)
// 点击确认
userEvent.click(screen.getByRole('button', { name: '确认' }))
// 弹窗文字为 '姓名不能为空'
expect(screen.getByRole('alert').children[0].innerHTML).toBe('姓名不能为空')
// 将时间快进 3s
jest.advanceTimersByTime(3000);
// 弹窗不存在于文档中
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
// 输入 '123' 并点击确认
userEvent.type(screen.getByRole('textbox'), '123')
userEvent.click(screen.getByRole('button', { name: '确认' }))
// 弹窗文字为 '提交成功'
expect(screen.getByRole('alert').children[0].innerHTML).toBe('提交成功')
})
运行,通过。
参考文章: