背景
测试就是有一个期望值,一个实际值,根据两者是否一致来判断是否通过。除了常规的逻辑测试外,前端还要对ui组件进行测试,对后者的测试,不同团队可能有不同的实现方式。比如
- 将组件逻辑剥离转化为普通的 js 代码测试(不直接测试 UI,这种方式会对正常代码组织影响很大)
- 以组件为单位测试具体的组件实例,比如调用实例方法和验证内部状态(比如Enzyme,由于过多关注实现细节,因此不方便维护)
- 不关注具体实现,而是像用户使用一样测试对应功能(后文会介绍的React Testing Library)。
相关概念
如果对前端自动化测试相关概念比较熟悉,可以跳过这一节。
前端测试包括四个部分
在这个测试奖杯中,测试被分为四类,从下到上依次测试的范围依次增大。
-
Static 用来静态检查,比如
eslint和typescript所处理的类型或语法错误,这部分一直在使用,基本保证了第三方库升级或小型重构后在解决完报错以后直接使用。 -
Unit 用来验证独立的代码片段能否正常工作
-
Integration 用来验证多个单元能否一起工作
-
End to End 用来像用户一样对应用程序操作,又叫做功能性测试
更具体的对比可以看这里。
本节参考
具体的实践
回到具体的开发过程,前端的单元测试和集成测试并没有什么明显的界限,当我们测试一个组件没必要区分这是不是最小测试单元,甚至react 官方将 react 组件的测试分成了Rendering component trees和Running a complete app两类,前者测试组件树,这被归类到单元测试中,后者即为 e2e。
又由于 e2e 工作量比较大,最好和测试一同完成,因此本次只讨论单元测试。
技术选型
关于不同的 UI 测试工具前文有所涉及,这里不会做更多对比, 而是由于我们开发使用的是react,因此我们会按照其推荐(单元测试进行选型,即
- 使用Jest作为测试框架
- 使用React Testing Library作为组件测试工具
其中 jest 是一个 js 测试框架(也被称为 test runner,用来执行测试脚本),React Testing Library 是使用在 jset 之上的一个测试 react 组件的 library。
jest
jest 提供了test 方法,或者用作it,用来写一个 test,可以用describe嵌套来组织不同的 test,比如
describe("test", () => {
let a = 1;
it("add1", () => {
a++;
expect(a).toEqual(2);
});
it("add1-again", () => {
a++;
expect(a).toEqual(3);
});
});
可以看到,在每个 test 中除了普通的 js 语句,还有expect方法用来验证被测试对象(expect 的参数)是否按期望(使用 expect 函数链式调用的方法来匹配)执行,每个 expect 方法被称为一个matcher。
当在命令行执行jest时会执行以上测试脚本,并显示如下结果
test
√ add1 (2 ms)
√ add1-again
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 5.922 s, estimated 6 s
再比如我们像验证一个函数,可以将其引入验证
//math.js
function sum(x, y) {
return x + y;
}
import sum from "./math.js";
describe("sum", () => {
test("sums up two values", () => {
expect(sum(2, 4)).toBe(6);
});
});
除了以上的基本用法外,jest还提供了测试异步方法、Mock 函数、快照等。
React Testing Library
刚才看到的jest提供了对普通 js 逻辑的验证,我们项目中更多的是UI,即 react 组件。
@testing-library家族以@testing-library/dom为核心包,封装了各种场景的测试工具,React Testing Library(以下简称 RTL),即@testing-library/react就是其中之一,其封装了 react 官方测试工具react-dom/test-utils。
RTL 的解决方案
当使用 RTL 进行测试时,是通过像用户使用一样,查找 dom 元素并与其交互的方式然后验证的方式来进行测试的,应避免测试实现细节,避免重构(修改实现但不修改功能)时造成测试用例失效。
比如
//counter.tsx
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount((c) => c + 1);
}
function decrement() {
setCount((c) => c - 1);
}
return (
<div>
<button onClick={decrement}>-</button>
<p>{count}</p>
<button onClick={increment}>+</button>
</div>
);
}
export default Counter;
//counter.test.js
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import Counter from "./counter";
describe("<Counter />", () => {
it("properly increments and decrements the counter", () => {
const { getByText } = render(<Counter />);
const counter = getByText("0");
const incrementButton = getByText("+");
const decrementButton = getByText("-");
fireEvent.click(incrementButton);
expect(counter.textContent).toEqual("1");
fireEvent.click(decrementButton);
expect(counter.textContent).toEqual("0");
});
});
具体使用方法
使用 RTL 的测试,步骤如下,
- 相关基础设置,包括 jest 配置文件,测试前后的准备工作,比如 mock 服务器的启动和关闭。我们会把每个组件作为一个测试对象来处理,组件以外的依赖都通过 mock 实现,比如
- 使用msw来 mock 对应 ajax 请求
- 测试无关的静态资源比如 css 或图片
- 运行环境缺失的 feature,比如
window.matchMedia
- 实际测试之前应该渲染出组件,
@testing-library/react提供了 render 方法。 - 渲染出组件后需要选择相关 dom 进行操作,这里我们使用上述 render 方法返回的方法,具体列表见这里。
- 选择完 dom 后,需要对其进行操作,可以使用@testing-library/user-event模拟对应的事件,然后使用@testing-library/jest-dom提供的 matcher 进行验证。
比如
test("失败请求用例", async () => {
server.use(
rest.get("/api/v1/agent_management/brief_account_info", (req, res, ctx) => {
return res(ctx.status(500));
})
);
const { getByText, getByRole } = render(
<FindingServer visible={true} onCancel={() => {}} />
);
userEvent.type(screen.getByTestId("input"), "13311112222");
const searchBtn = getByText("查 询");
fireEvent.click(searchBtn);
await waitFor(() =>
expect(getByRole(/search/)).not.toHaveClass("ant-btn-loading")
);
expect(screen.queryByText(/No Data/)).toBeInTheDocument();
});