前言

我想大部分人的前端测试,都是运行项目,直接在浏览器上操作,看看功能正不正常。虽然明明有测试库可以使用,但是因为“要快”的原因,让好好做测试变成了一件影响效率的事。

因为这种无奈的原因而放弃测试,实在是很可惜。这种原因也并不能够说明测试没有必要,测试仍然是需要重视的东西。

我将简单介绍如何在 React 中进行单测。本文中使用的代码仍然是通过 vite 创建的 React-ts 项目,所以可能不适用于其他的项目。

我们需要什么东西?

我们需要安装几个包,很烦。每个包的功能当然是不一样的,更难受的是这些测试库既然依赖于其他包的功能,为什么不干脆集成在一起呢。

我先总的介绍一下这几个包的关系:

  1. vitest:单测库,用于自动运行测试代码,下面介绍的几个包,会通过 vitest 运行起来。
  2. @testing-library/react:testing-library 是个 UI 测试库,用于在测试中模仿浏览器渲染组件,@testing-library/react 是指适用于 react 的版本。
  3. happy-dom:用于在测试中提供浏览器的 document 功能,如果没有这个包,测试中会抛出 document is not defined,所以我们需要提供这个 document。
  4. msw:提供 mock 功能的库。如果你测试的组件中有发起请求,那么在测试中需要 mock 这些请求。如果没有发起请求,也可以无需要这个 mock 库。在我之前的文章 React 简单教程-5-使用 mock 中有介绍过这个库在开发时的用法,但是开发时的 mock 用法跟我们测试时的 mock 用法是不同的。

没错,上面这几个包都是我们需要安装的。因为是在开发时使用,所以都在安装到 devDependencies 下。这几个包你都可以直接搜索名字方便地找到官网。

都安装好后,我们就开始配置了。

配置

使用 vitest 的好处之一就是节省一个配置文件。vitest 的配置可以写在 vite.config.ts 文件中。

在 vite.config.ts 文件的 defineConfig 中添加 test 节点,这个节点就是我们 vitest 的配置。

上面代码的第一行是 ts 的三斜线指令,功能是用于引入 vitest。如果没有这一句,编译器会警告你没有 test 字段的定义。因为我们的 test 字段是 vitest 提供的功能,所以类型声明也需要由它提供。

接下来看 test 字段。先看 environment,这个字段用于配置我们测试运行时的环境,这里的值为我们安装了的 happy-dom。如果没有配置这个字段,运行测试将出现 document is not defined 的错误。

在项目根目录下新建文件夹 test,我们有关测试的代码都将放在这个文件夹下面。

好的,配置的部分完毕,现在我们简单弄一个组件,然后来测试一下。

要测试的组件

我们要测试的组件代码如下,我直接放在了 src/displayer.tsx 里:

相关的样式代码如下:

这个组件的功能是点击左上角黄色的按钮,会发起一个请求,点击灰色的按钮会隐藏下方的内容。那么我们要测试的功能,就有两个:

  1. 点击灰色按钮,查看内容元素有没有隐藏
  2. 点击黄色按钮,查看有没有发起请求,需要 MOCK

由于这是同一个组件的测试,所以我们可以写到一个测试文件里,测试文件的名字也有讲究,我们使用 ts 编写,所以必须以 .test.tsx 或 .spec.tsx结尾。

在 test 文件夹中新建 displayer.test.tsx 测试文件,当我们使用 vitest 测试时,vitest 会自动找到这些测试文件运行。

导入我们需要的测试套件:

上面的 describe() 方法用于定义一套测试内容,第一个参数是名字,你可以起这套测试的名字,第二个参数测试内容,你能够看到内容中我使用 it() 方法,这个方法用于定义具体的测试内容。

所以你看,其实 describe() 可以不使用,直接使用 it 定义测试也可以的。运行测试会执行 it 方法。

看我们代码里的 it 方法,第一个参数是测试名,用于简单描述测试什么,"load and click gray button",描述了我们将加载这个组件并点击灰色按钮。第二个参数就是测试代码了。那么测试代码里应该怎么写呢?

一般情况下,测试代码都会遵循这个三个范式:

Arrange 测试前的准备,在这里,我们要先把组件渲染了;Act 测试行为,在这里,我们要模仿点击按钮的行为;Assert 测试断言,在这里,我要检查点击按钮后组件是否达到预期的行为。

那么我们便照着这三步骤来。

测试点击灰色按钮

在测试中渲染组件,需要导入 @testing-library/react 的 render 方法,和我们的组件。然后使用 render 渲染:

render() 方法返回了一些方法,我们可以使用这些方法来获取组件里的信息,在断言时很有用。更多关于 render() 和返回值的信息查看 这里官网。

下来要模拟点击灰色按钮,组件点击事件的触发需要导入 @testing-library/react 的 fireEvent。

这里简单使用了 fireEvent.click 方法来模拟点击,该方法需要的参数是点击的元素,那么我们怎么获取到元素呢?

代码里我们使用了 getByTestId(...) 方法获取,这个方法会指定带有属性 data-testid 的元素,如上面我们使用 getByTestId("gray") 获取元素属性data-testid 的值为 gray 的元素。关于更多查询元素的 API,这里查看。

模拟点击后,我们便要检查组件点击后的行为是否正确,就到了断言这一步了。

在我们的组件里,点击灰色按钮后,内容元素(这个元素我标记了 role 属性为 displayer-content)将会被卸载,所以我们要尝试获取这个元素,预料中是获取不到的。

我们使用了 queryByRole() 来通过元素的 role 属性获取元素,注意我们使用的查询方法是 query... 开头的,在 testing-library 的规范中,query... 开头的方法在获取不到元素是会返回 null。关于更详细的查询方法规范查看这里。

最后我们使用了 assert 断言,判断 body 元素应该是为空。如果断言通过,则测试通过,否则测试失败。

现在就来运行测试,先在 package.json 里配置测试的脚本:

该脚本将会运行 vitest 命令来启动测试,vitest 相当于 vitest watch,运行此命令,当我们修改了测试代码,就会自动测试修改的代码。在终端中输入 npm run test,你应该会看到如下信息:

image.png

这种和谐美满的输出表示测试成功。如果是下面这种充满铁和血的画面:

image.png

表示测试失败,还贴心地告诉你哪里失败。

需要 mock 的测试,点击黄色按钮

上面我们测试了组件的行为,但是如果有发起请求的事件,我们要怎么测试呢?当然是使用 mock 了,我上一章讲过 mock 库在开发时如何使用,React 简单教程-5-使用 mock。我们使用同一个 mock 库,但在测试时使用 mock 和开发时方法不同,单元测试时我们不需要拦截浏览器行为。

我们要测试的组件中,点击黄色按钮后会发起一个请求,这个行为就是我们需要 mock 的。思路就是,运行测试前启动 mock,测试结束后关闭 mock。

vitest 提供了两个方法,beforeAll() 将在所有测试开始前运行传入的方法,afterAll()将在所有测试开始后运行传入的方法。

先直接在测试文件中安装 mock 对象,:

如上,我们伪造的假响应 mockObj和它的字符串形式 mockResult,在 beforeAll() 中启动 mock,在 afterAll 中关闭 mock。

先回过头看组件中黄色按钮的实现:我们使用 fetch 发起了请求——注意!单测中的请求地址必须为完整地址,mock 中的也一样,然后将请求结果在内容元素中显示。由于我们 mock 了假响应,所以内容元素中显示的应该会是我们提供的假数据。

那么思路就有了,渲染组件,点击黄色按钮,找找看有没有假数据的信息。

看,我们使用了一个新的方法 waitFor(),这个方法接收另一个返回元素的方法,waitFor() 会一直尝试获取,直到获取到了或者超时,默认的超时事件是 1 秒,拿到元素后就将元素返回。

在 waitFor() 中我们使用了 getByText(),get... 开头的方法如果拿不到元素就会抛出异常。由于我们是查找有没有包含我们提供的假数据的元素,所以,如果没有抛出异常的话就是找到了。最后的断言 assert.isNotNull(e); 也是可以不用的。

一定要亲自试一试。

两次一起测试的问题

写了两个测试,现在运行起来的话你会看到如下的错误:

image.png

下方还有详细错误信息和组件树的结构,意思是说你使用了 getByTestId("yellow") 获取元素,这个方法预期是只有一个元素的,现在拿到了多个,于是报错了。

不对啊,我们的组件中只有一个黄色按钮!详细看看错误信息显示出来的组件树,赫然有两个组件!

发生这种事的原因是测试时渲染组件后,会将组件放在一个虚拟的 dom 环境中测试,在我们的一个 it 测试用例中,测试后没有将这个组件清理了,就导致下一个测试用例需要渲染同一个组件,就重复添加了组件到虚拟的 dom 环境中。

解决这个问题,要用到 vitest 提供的另一个方法 afterEach(),这个方法将在每一个测试用例结束后执行,使用这个 cleanup() 清理渲染的组件。

然后测试便可以正常执行。

完整测试代码

参考资料

testing-library/react by testing-library

vitest by Vitest

msw by msw

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注