React 组件和 hook 如何写单元测试?
当你写完一个 React 组件,如何保证它的功能是正常的呢?
在浏览器里渲染出来,手动测试一遍就好了啊。
那如果这个组件交给别人维护了,他并不知道这个组件的功能应该是什么样的,怎么保证他改动代码之后,组件功能依然正常?
这种情况就需要单元测试了。
单元测试可以测试函数、类的方法等细粒度的代码单元,保证功能正常。
有了单元测试之后,后续代码改动只需要跑一遍单元测试就知道功能是否正常。
但很多同学觉得单元测试没意义,因为代码改动比较频繁,单元测试也跟着需要频繁改动。
确实,如果代码改动特别频繁,就没必要单测了,手动测试就好。
因为如果手动测试一遍需要 5 分钟,写单元测试可能需要一个小时。
但如果代码比较稳定,那单测还是很有必要的,比如组件库里的组件、hooks 库里的 hooks、一些工具函数等。
手动测试 5 分钟,每次都要手动测试,假设 20 次,那就是 100 分钟的成本,而且还不能保证测试是可靠的。
写单测要一个小时,每次直接跑单测自动化测试,跑 100 次也是一个小时的成本,而且还是测试结果很可靠。
综上,单元测试能保证函数、类的方法等代码单元的功能正常,把手动测试变成自动化测试。
但是写单元测试成本还是挺高的,如果代码改动频繁,那手动测试更合适。一些比较稳定的代码,还是有必要写单测的,写一次,自动测试 n 次,收益很大。
那 React 的组件和 hooks 怎么写单测呢?
这篇文章我们一起来写几个单测试试。
用 create-react-app 创建个 react 项目:
npx create-react-app --template=typescript react-unit-test
测试 react 组件和 hooks 可以使用 @testing-library/react 这个包,然后测试用例使用 jest 来组织。
这两个包 cra 都给引入了,我们直接跑下 npm run test 就可以看到单测结果。
App 组件是这样的:
它的单测是这么写的:
通过 @testing-library/react 的 render 函数把组件渲染出来。
通过 screen 来查询 dom,查找文本内容匹配正则 /learn react/ 的 a 标签。
然后断言它在 document 内。
你也可以这么写:
test('renders learn react link 2', () => {
const { container } = render(<App />);
const linkElement = container.querySelector('.App-link');
expect(linkElement?.textContent).toMatch(/learn react/i)
});
render 会返回组件挂载的容器 dom,它是一个 HTMLElement 的对象,有各种 dom 方法。
可以用 querySelector 查找到那个 a 标签,然后判断它的内容是否匹配正则。
这两种写法都可以。
第二种方法更容易理解,就是拿到渲染容器的 dom,再用 dom api 来查找 dom。
第一种方法的 screen 是 @testing-library/react 提供的 api,是从全局查找 dom,可以直接根据文本查(getByText),根据标签名和属性查(getByRole) 等。
其实 render 方法返回的对象里也有这些 api:
个人感觉没啥必要用这种 api,直接拿到 container dom 再做 dom 操作的方式就好了。
antd 组件的测试也是用的这种:
那如果有 onClick、onChange 等事件监听器的组件,怎么测试呢?
我们写个组件:
import { useCallback, useState } from 'react';
function Toggle() {
const [status, setStatus] = useState(false);
const clickHandler = useCallback(() => {
setStatus((prevStatus) => !prevStatus);
}, []);
return (
<div>
<button onClick={clickHandler}>切换</button>
<p>{status ? 'open' : 'close' }</p>
</div>
);
}
export default Toggle;
渲染出来是这样的:
这个组件如何测试呢?
需要用到 firEvent 方法了。
import { render, fireEvent } from '@testing-library/react';
import Toggle from './Toggle';
test('toggle', () => {
const { container } = render(<Toggle/>);
expect(container.querySelector('p')?.textContent).toBe('close');
fireEvent.click(container.querySelector('button')!)
expect(container.querySelector('p')?.textContent).toBe('open');
})
用 render 方法把组件渲染出来。
用 container 节点的 dom api 查询 p 标签的文本,断言是 close。
然后用 fireEvent.click 触发 button 的点击事件。
断言 p 标签的文本是 open。
测试通过了:
fireEvent 可以触发任何元素的任何事件:
那如何触发 change 事件呢?
这样写:
第二个参数传入 target 的 value 值。
除了 fireEvent.xxx 来触发 xxx 事件外,你还可以 fireEvent 传入自定义事件。
直接 new Event,然后用 fireEvent 传给某个元素。
或者用 createEvent 创建某个事件,再 fireEvent 传给某个元素。
这个 createEvent 也是 @testing-library/react 提供的 api,用来创建事件的。
绝大多数情况下,直接 fireEvent.xxx 就好了。
此外,如果我有段异步逻辑,过段时间才会渲染内容,这时候怎么测呢?
比如我点击按钮之后,过了 2s 才改状态:
这时候测试用例就报错了:
这种用 waitFor 包裹下,设置 timeout 的时间就好了:
测试通过了:
除了这些之外,还有一个 api 比较常用,就是 act
它是 react-dom 包里的,@testing-library/react 对它做了一层包装。
就是把触发更新的代码放到 act 里面去执行,之后再断言。
文档里的例子是这样的:
其实刚才我们的 fireEvent 就应该放到 act 里包一层。
结果一样:
不过大多数情况下,不用 act 包裹也没啥问题。
组件测试我们学会了,那如果我想单独测试 hooks 呢?
这就要用到 renderHook 的 api 了。
直接看下 ahooks 的单测:
renderHook 第一个参数的函数里调用各种 hook,返回的 result.current 就是 hook 的返回值。
触发事件也是用 fireEvent:
这就是 hooks 的单测写法。
总结
单元测试能保证函数、类的方法等代码单元的功能正常,把手动测试变成自动化测试。
变更不频繁的代码,还是有必要写单测的,写一次,自动测试 n 次,收益很大。
我们学了 react 组件和 hook 的单测写法。
主要是用 @testing-library/react 这个库,它有一些 api:
- render:渲染组件,返回 container 容器 dom 和其他的查询 api
- fireEvent:触发某个元素的某个事件
- createEvent:创建某个事件
- waitFor:等待异步操作完成再断言,可以指定 timeout
- act:包裹的代码会更接近浏览器里运行的方式
- renderHook:执行 hook,可以通过 result.current 拿到 hook 返回值
其实也没多少东西。
jest 的 api 加上 @testing-libary/react 的这些 api,就可以写任何组件、hook 的单元测试了。
转载自:https://juejin.cn/post/7259329671224655933