likes
comments
collection
share

React 组件和 hook 如何写单元测试?

作者站长头像
站长
· 阅读数 8

当你写完一个 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 组件和 hook 如何写单元测试?

测试 react 组件和 hooks 可以使用 @testing-library/react 这个包,然后测试用例使用 jest 来组织。

这两个包 cra 都给引入了,我们直接跑下 npm run test 就可以看到单测结果。

React 组件和 hook 如何写单元测试?

App 组件是这样的:

React 组件和 hook 如何写单元测试?

它的单测是这么写的:

React 组件和 hook 如何写单元测试?

通过 @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 方法。

React 组件和 hook 如何写单元测试?

可以用 querySelector 查找到那个 a 标签,然后判断它的内容是否匹配正则。

这两种写法都可以。

第二种方法更容易理解,就是拿到渲染容器的 dom,再用 dom api 来查找 dom。

第一种方法的 screen 是 @testing-library/react 提供的 api,是从全局查找 dom,可以直接根据文本查(getByText),根据标签名和属性查(getByRole) 等。

其实 render 方法返回的对象里也有这些 api:

React 组件和 hook 如何写单元测试?

个人感觉没啥必要用这种 api,直接拿到 container dom 再做 dom 操作的方式就好了。

antd 组件的测试也是用的这种:

React 组件和 hook 如何写单元测试?

那如果有 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;

渲染出来是这样的:

React 组件和 hook 如何写单元测试?

这个组件如何测试呢?

需要用到 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。

测试通过了:

React 组件和 hook 如何写单元测试?

fireEvent 可以触发任何元素的任何事件:

React 组件和 hook 如何写单元测试?

那如何触发 change 事件呢?

这样写:

React 组件和 hook 如何写单元测试?

第二个参数传入 target 的 value 值。

除了 fireEvent.xxx 来触发 xxx 事件外,你还可以 fireEvent 传入自定义事件。

React 组件和 hook 如何写单元测试?

直接 new Event,然后用 fireEvent 传给某个元素。

或者用 createEvent 创建某个事件,再 fireEvent 传给某个元素。

这个 createEvent 也是 @testing-library/react 提供的 api,用来创建事件的。

React 组件和 hook 如何写单元测试?

绝大多数情况下,直接 fireEvent.xxx 就好了。

此外,如果我有段异步逻辑,过段时间才会渲染内容,这时候怎么测呢?

比如我点击按钮之后,过了 2s 才改状态:

React 组件和 hook 如何写单元测试?

这时候测试用例就报错了:

React 组件和 hook 如何写单元测试?

这种用 waitFor 包裹下,设置 timeout 的时间就好了:

React 组件和 hook 如何写单元测试?

测试通过了:

React 组件和 hook 如何写单元测试?

除了这些之外,还有一个 api 比较常用,就是 act

它是 react-dom 包里的,@testing-library/react 对它做了一层包装。

React 组件和 hook 如何写单元测试?

就是把触发更新的代码放到 act 里面去执行,之后再断言。

文档里的例子是这样的:

React 组件和 hook 如何写单元测试?

其实刚才我们的 fireEvent 就应该放到 act 里包一层。

React 组件和 hook 如何写单元测试?

结果一样:

React 组件和 hook 如何写单元测试?

不过大多数情况下,不用 act 包裹也没啥问题。

组件测试我们学会了,那如果我想单独测试 hooks 呢?

这就要用到 renderHook 的 api 了。

直接看下 ahooks 的单测:

React 组件和 hook 如何写单元测试?

renderHook 第一个参数的函数里调用各种 hook,返回的 result.current 就是 hook 的返回值。

触发事件也是用 fireEvent:

React 组件和 hook 如何写单元测试?

这就是 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 的单元测试了。