React简单教程-6-单元测试
前言
我想大部分人的前端测试,都是运行项目,直接在浏览器上操作,看看功能正不正常。虽然明明有测试库可以使用,但是因为“要快”的原因,让好好做测试变成了一件影响效率的事。
因为这种无奈的原因而放弃测试,实在是很可惜。这种原因也并不能够说明测试没有必要,测试仍然是需要重视的东西。
我将简单介绍如何在 React 中进行单测。本文中使用的代码仍然是通过 vite
创建的 React-ts 项目,所以可能不适用于其他的项目。
我们需要什么东西?
我们需要安装几个包,很烦。每个包的功能当然是不一样的,更难受的是这些测试库既然依赖于其他包的功能,为什么不干脆集成在一起呢。
我先总的介绍一下这几个包的关系:
- vitest:单测库,用于自动运行测试代码,下面介绍的几个包,会通过 vitest 运行起来。
- @testing-library/react:testing-library 是个 UI 测试库,用于在测试中模仿浏览器渲染组件,@testing-library/react 是指适用于 react 的版本。
- happy-dom:用于在测试中提供浏览器的
document
功能,如果没有这个包,测试中会抛出document is not defined
,所以我们需要提供这个document
。 - msw:提供 mock 功能的库。如果你测试的组件中有发起请求,那么在测试中需要 mock 这些请求。如果没有发起请求,也可以无需要这个 mock 库。在我之前的文章 React 简单教程-5-使用 mock 中有介绍过这个库在开发时的用法,但是开发时的 mock 用法跟我们测试时的 mock 用法是不同的。
没错,上面这几个包都是我们需要安装的。因为是在开发时使用,所以都在安装到 devDependencies
下。这几个包你都可以直接搜索名字方便地找到官网。
npm i -D vitest
npm i -D @testing-library/react
npm i -D happy-dom
npm i -D msw
都安装好后,我们就开始配置了。
配置
使用 vitest 的好处之一就是节省一个配置文件。vitest 的配置可以写在 vite.config.ts
文件中。
在 vite.config.ts
文件的 defineConfig
中添加 test
节点,这个节点就是我们 vitest
的配置。
/// <reference types="vitest" />
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
environment: "happy-dom",
},
});
上面代码的第一行是 ts 的三斜线指令,功能是用于引入 vitest
。如果没有这一句,编译器会警告你没有 test
字段的定义。因为我们的 test
字段是 vitest
提供的功能,所以类型声明也需要由它提供。
接下来看 test
字段。先看 environment
,这个字段用于配置我们测试运行时的环境,这里的值为我们安装了的 happy-dom
。如果没有配置这个字段,运行测试将出现 document is not defined
的错误。
在项目根目录下新建文件夹 test
,我们有关测试的代码都将放在这个文件夹下面。
好的,配置的部分完毕,现在我们简单弄一个组件,然后来测试一下。
要测试的组件
我们要测试的组件代码如下,我直接放在了 src/displayer.tsx
里:
import { useState } from "react";
import "./displayer.css";
export function Displayer(props: { name: string, content: string }) {
const name = props.name;
const [hidden, setHidden] = useState(false);
const [content, setContent] = useState(props.content);
function handleClick() {
setHidden(true);
}
async function handleRequest() {
const result = await fetch("https://api.backend.dev/getSth");
const j = await result.json();
setContent(JSON.stringify(j));
}
return (
<div className="container">
<div className="nav">
<span className="btn-red"></span>
<button
className="btn-yellow"
data-testid="yellow"
onClick={handleRequest}
></button>
<button
className="btn-gray"
data-testid="gray"
onClick={handleClick}
></button>
</div>
{hidden || (
<div className="body" role="displayer-content">
<div>{name}</div>
<div>{content}</div>
</div>
)}
</div>
);
}
相关的样式代码如下:
.container {
background-color: rgb(31 41 55 / 1);
background-color: rgb(31 41 55 /1);
padding: 0.5rem;
margin: 0.5rem;
border-radius: 0.5rem;
}
.nav {
display: flex;
margin-bottom: 0.3rem;
}
.btn-red {
height: 12px;
width: 12px;
background-color: #ff1d1d;
border-radius: 15px;
}
.btn-yellow {
height: 12px;
width: 12px;
background-color: rgb(255, 251, 29);
border-radius: 15px;
margin: auto 0.5rem;
}
.btn-gray {
height: 12px;
width: 12px;
background-color: rgb(220, 220, 220);
border-radius: 15px;
}
.body {
padding: 0.5rem 0;
}
这个组件的功能是点击左上角黄色的按钮,会发起一个请求,点击灰色的按钮会隐藏下方的内容。那么我们要测试的功能,就有两个:
- 点击灰色按钮,查看内容元素有没有隐藏
- 点击黄色按钮,查看有没有发起请求,需要 MOCK
由于这是同一个组件的测试,所以我们可以写到一个测试文件里,测试文件的名字也有讲究,我们使用 ts 编写,所以必须以 .test.tsx
或 .spec.tsx
结尾。
在 test 文件夹中新建 displayer.test.tsx
测试文件,当我们使用 vitest
测试时,vitest
会自动找到这些测试文件运行。
导入我们需要的测试套件:
import { assert, describe, it } from "vitest";
describe("test displayer", () => {
it("load and click gray button", () => {});
it("load and click yellow button", () => {});
});
上面的 describe()
方法用于定义一套测试内容,第一个参数是名字,你可以起这套测试的名字,第二个参数测试内容,你能够看到内容中我使用 it()
方法,这个方法用于定义具体的测试内容。
所以你看,其实 describe()
可以不使用,直接使用 it
定义测试也可以的。运行测试会执行 it
方法。
看我们代码里的 it
方法,第一个参数是测试名,用于简单描述测试什么,"load and click gray button"
,描述了我们将加载这个组件并点击灰色按钮。第二个参数就是测试代码了。那么测试代码里应该怎么写呢?
一般情况下,测试代码都会遵循这个三个范式:
it("load and click gray button", () => {
// Arrange
// Act
// Assert
});
Arrange
测试前的准备,在这里,我们要先把组件渲染了;Act
测试行为,在这里,我们要模仿点击按钮的行为;Assert
测试断言,在这里,我要检查点击按钮后组件是否达到预期的行为。
那么我们便照着这三步骤来。
测试点击灰色按钮
在测试中渲染组件,需要导入 @testing-library/react
的 render
方法,和我们的组件。然后使用 render
渲染:
import { render } from "@testing-library/react";
import { Displayer } from "../src/displayer";
it("load and click gray button", () => {
// Arrange
const { getByTestId } = render(
<Displayer name="test name" content="test content" />
);
});
render()
方法返回了一些方法,我们可以使用这些方法来获取组件里的信息,在断言时很有用。更多关于 render()
和返回值的信息查看 这里官网。
下来要模拟点击灰色按钮,组件点击事件的触发需要导入 @testing-library/react
的 fireEvent
。
import { render, fireEvent } from "@testing-library/react";
it("load and click gray button", () => {
// Arrange
const { getByTestId } = render(
<Displayer name="test name" content="test content" />
);
// Act
fireEvent.click(getByTestId("gray"));
});
这里简单使用了 fireEvent.click
方法来模拟点击,该方法需要的参数是点击的元素,那么我们怎么获取到元素呢?
代码里我们使用了 getByTestId(...)
方法获取,这个方法会指定带有属性 data-testid
的元素,如上面我们使用 getByTestId("gray")
获取元素属性data-testid
的值为 gray
的元素。关于更多查询元素的 API,这里查看。
模拟点击后,我们便要检查组件点击后的行为是否正确,就到了断言这一步了。
在我们的组件里,点击灰色按钮后,内容元素(这个元素我标记了 role
属性为 displayer-content
)将会被卸载,所以我们要尝试获取这个元素,预料中是获取不到的。
import { render, fireEvent } from "@testing-library/react";
import { assert, describe, it } from "vitest";
it("load and click gray button", () => {
// Arrange
const { getByTestId, queryByRole } = render(
<Displayer name="test name" content="test content" />
);
// Act
fireEvent.click(getByTestId("gray"));
// Assert
const body = queryByRole("displayer-content");
assert.isNull(body);
});
我们使用了 queryByRole()
来通过元素的 role
属性获取元素,注意我们使用的查询方法是 query...
开头的,在 testing-library
的规范中,query...
开头的方法在获取不到元素是会返回 null
。关于更详细的查询方法规范查看这里。
最后我们使用了 assert
断言,判断 body
元素应该是为空。如果断言通过,则测试通过,否则测试失败。
现在就来运行测试,先在 package.json
里配置测试的脚本:
{
"scripts": {
"test": "vitest"
},
}
该脚本将会运行 vitest
命令来启动测试,vitest
相当于 vitest watch
,运行此命令,当我们修改了测试代码,就会自动测试修改的代码。在终端中输入 npm run test
,你应该会看到如下信息:
这种和谐美满的输出表示测试成功。如果是下面这种充满铁和血的画面:
表示测试失败,还贴心地告诉你哪里失败。
需要 mock 的测试,点击黄色按钮
上面我们测试了组件的行为,但是如果有发起请求的事件,我们要怎么测试呢?当然是使用 mock 了,我上一章讲过 mock 库在开发时如何使用,React 简单教程-5-使用 mock。我们使用同一个 mock 库,但在测试时使用 mock 和开发时方法不同,单元测试时我们不需要拦截浏览器行为。
我们要测试的组件中,点击黄色按钮后会发起一个请求,这个行为就是我们需要 mock 的。思路就是,运行测试前启动 mock,测试结束后关闭 mock。
vitest
提供了两个方法,beforeAll()
将在所有测试开始前运行传入的方法,afterAll()
将在所有测试开始后运行传入的方法。
先直接在测试文件中安装 mock 对象,:
// 省略
const mockObj = { userName: "admin" };
const mockResult = JSON.stringify(mockObj);
const mockServer = setupServer(
rest.get("https://api.backend.dev/getSth", (req, res, ctx) => {
return res(ctx.json(mockObj));
})
);
beforeAll(() => {
mockServer.listen();
});
afterAll(() => {
mockServer.close();
});
// 省略
如上,我们伪造的假响应 mockObj
和它的字符串形式 mockResult
,在 beforeAll()
中启动 mock,在 afterAll
中关闭 mock。
先回过头看组件中黄色按钮的实现:我们使用 fetch
发起了请求——注意!单测中的请求地址必须为完整地址,mock 中的也一样,然后将请求结果在内容元素中显示。由于我们 mock 了假响应,所以内容元素中显示的应该会是我们提供的假数据。
那么思路就有了,渲染组件,点击黄色按钮,找找看有没有假数据的信息。
import { render, fireEvent, waitFor } from "@testing-library/react";
it("load and click yellow button", async () => {
// Arrange
const { getByTestId, getByText } = render(
<Displayer name="test name" content="test content" />
);
// Act
fireEvent.click(getByTestId("yellow"));
const e = await waitFor(() => getByText(mockResult));
// Assert
assert.isNotNull(e);
});
看,我们使用了一个新的方法 waitFor()
,这个方法接收另一个返回元素的方法,waitFor()
会一直尝试获取,直到获取到了或者超时,默认的超时事件是 1 秒,拿到元素后就将元素返回。
在 waitFor()
中我们使用了 getByText()
,get...
开头的方法如果拿不到元素就会抛出异常。由于我们是查找有没有包含我们提供的假数据的元素,所以,如果没有抛出异常的话就是找到了。最后的断言 assert.isNotNull(e);
也是可以不用的。
一定要亲自试一试。
两次一起测试的问题
写了两个测试,现在运行起来的话你会看到如下的错误:
下方还有详细错误信息和组件树的结构,意思是说你使用了 getByTestId("yellow")
获取元素,这个方法预期是只有一个元素的,现在拿到了多个,于是报错了。
不对啊,我们的组件中只有一个黄色按钮!详细看看错误信息显示出来的组件树,赫然有两个组件!
发生这种事的原因是测试时渲染组件后,会将组件放在一个虚拟的 dom 环境中测试,在我们的一个 it
测试用例中,测试后没有将这个组件清理了,就导致下一个测试用例需要渲染同一个组件,就重复添加了组件到虚拟的 dom 环境中。
解决这个问题,要用到 vitest
提供的另一个方法 afterEach()
,这个方法将在每一个测试用例结束后执行,使用这个 cleanup()
清理渲染的组件。
afterEach(() => {
cleanup();
});
然后测试便可以正常执行。
完整测试代码
import { render, fireEvent, waitFor, cleanup } from "@testing-library/react";
import { afterAll, afterEach, assert, beforeAll, describe, it } from "vitest";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { Displayer } from "../src/displayer";
const mockObj = { userName: "admin" };
const mockResult = JSON.stringify(mockObj);
const mockServer = setupServer(
rest.get("https://api.backend.dev/getSth", (req, res, ctx) => {
return res(ctx.json(mockObj));
})
);
beforeAll(() => {
mockServer.listen();
});
afterAll(() => {
mockServer.close();
});
afterEach(() => {
cleanup();
});
describe("test displayer", () => {
it("load and click gray button", () => {
// Arrange
const { getByTestId, queryByRole } = render(
<Displayer name="test name" content="test content" />
);
// Act
fireEvent.click(getByTestId("gray"));
// Assert
const body = queryByRole("displayer-content");
assert.isNull(body);
});
it("load and click yellow button", async () => {
// Arrange
const { getByTestId, getByText } = render(
<Displayer name="test name" content="test content" />
);
// Act
fireEvent.click(getByTestId("yellow"));
const e = await waitFor(() => getByText(mockResult));
// Assert
assert.isNotNull(e);
});
});
参考资料
testing-library/react by testing-library
vitest by Vitest
msw by msw
React简单教程-6-单元测试的更多相关文章
- React简单教程-4-事件和hook
前言 在上一章 React 简单教程-3-样式 中我们建立了一个子组件,并稍微美化了一下.在另一篇文章 React 简单教程-3.1-样式之使用 tailwindcss 章我们使用了 tailwind ...
- React简单教程-3-样式
前言 在上一章 React 简单教程-2-ts 和组件参数 中我们新建的子组件 Displayer 没有样式,显得平平无奇,这一篇我们将给他美化一下. CSS 文件 一般的做法,是在你的组件级目录下新 ...
- React简单教程-2-ts和组件参数
前言 在上一章:React 简单教程-1-组件 我们知道了 React 的组件是什么,长什么样,用 js 和 HTML 小小体验了一下组件.在这一章,我们将使用 typescript(简称 ts) 来 ...
- React简单教程-4.1-hook
前言 虽然我们简单感受了一下 useState 的用法,但我想你还是对 React 里的 hook 迷迷糊糊的.本文我们将明确下 React 的概念. HOOK 前生今世 在我示例中,写的 React ...
- React简单教程-1-组件
前言 React,Facebook开发的前端框架.当时Facebook对市面上的前端框架都不满意,于是自己捣鼓出了React,使用后觉得特别好用,于是就在2013年开源了. 我也用React开发了一个 ...
- React简单教程-5-使用mock
前言 一个前后端分离的项目,前端人员需要对接后端的接口.如果在后端的接口没有开发好,或者没有测试版可以对接的情况下,前端人员也不能坐等后端接口写好后再开始开发. 一个项目的,理想情况下接口的规范应该是 ...
- React简单教程-3.1-样式之使用 tailwindcss
前言 本文是作为一个额外内容,主要介绍 tailwindcss 的用法 tailwindcss 是一个功能类优先的 CSS 框架,我在以前的文章里有描述为什么使用功能类优先:为什么我在 css 里使用 ...
- react+redux教程(五)异步、单一state树结构、componentWillReceiveProps
今天,我们要讲解的是异步.单一state树结构.componentWillReceiveProps这三个知识点. 例子 这个例子是官方的例子,主要是从Reddit中请求新闻列表来显示,可以切换reac ...
- react+redux教程(四)undo、devtools、router
上节课,我们介绍了一些es6的新语法:react+redux教程(三)reduce().filter().map().some().every()....展开属性 今天我们通过解读redux-undo ...
随机推荐
- github账号&文章选题
----------------------------------------------------------- https://github.com/yanpanjiao github ...
- 关于vue中v-for的键值顺序
在学习vue2.0时,关于处理v-for键值顺序时发现的问题: <body> <!-- 普通循环 --> <!-- {{num}} --> <!-- 列表循环 ...
- 7.Docker容器使用辅助工具汇总
原文地址: 点击直达 more information: https://docs.docker.com/engine/security/security/#docker-daemon-attack- ...
- [ Perl ] 多线程并发编程
https://www.cnblogs.com/yeungchie/ 记录一些常用的 模块 / 方法 . 多线程 使用模块 threads use 5.010; use threads; sub fu ...
- 数据传输POST心法分享,做前端的你还解决不了这个bug?
背景 随时随地给大家提供技术支持的葡萄又来了.这次的事情是这样的,提供demo属于是常规操作,但是前两天客户突然反馈压缩传输模块抛出异常,具体情况是压缩内容传输到服务端后无法解压. 由于代码没有发生任 ...
- 【大话云原生】kubernetes灰度发布篇-从步行到坐缆车的自动化服务升级
此文系[大话云原生]系列第四篇,该系列文章期望用最通俗.简单的语言说明白云原生生态系统内的组成.架构以及应用关系.从这篇开始我们要开始针对Kubernetes进行介绍了,本文内容如下: 一.Kuber ...
- XCTF练习题---CRYPTO---告诉你个秘密
XCTF练习题---CRYPTO---告诉你个秘密 flag:TONGYUAN 步骤解读: 1.观察题目,下载附件 2.打开附件,内容好像有点像十六进制,先进行一下十六进制转换,得到一串字符 网址:h ...
- WIN10 使用POWERSHELL 设置单应用KIOSK模式(win10家庭版或企业版)
win10 使用PowerShell 设置单应用kiosk模式 win10 家版或企业版PowerShellshell 启动器 v1Autologon.exe 注意事项 win10 家庭版或企业版. ...
- VMware16搭建Ubuntu22.04,更新为国内下载源,安装open-vm-tools,用SecureCRT远程连接
前期准备 1.VMware16(转载:下载安装流程:(https://www.bilibili.com/read/cv9694457)) 2.Ubuntu22.04----iso镜像文件(下载地址:( ...
- [笔记] $f(i)$ 为 $k$ 次多项式,$\sum_{i=0}^nf(i)\cdot q^i$ 的 $O(k\log k)$ 求法
\(f(i)\) 为 \(k\) 次多项式,\(\sum_{i=0}^nf(i)\cdot q^i\) 的 \(O(k\log k)\) 求法 令 \(S(n)=\sum_{i=0}^{n-1}f(i ...