用 Vitest 与 React Testing Library 测试前端¶
这一章讲什么: 前端代码怎么自动化验证。三件东西各管一段: Vitest 负责「跑」测试,React Testing Library 负责「像用户一样」 检查渲染结果,mock 负责把不想真调用的部分(网络、模块、函数)换成替身。 读完你能写出一个会打字、会点按钮、断言屏幕上出现了什么的组件测试, 也能看懂别人代码里的四层 mock 各是什么来头。
1. 测试运行器与第一个测试¶
测试运行器(test runner)是命令行程序:扫遍项目、找出所有测试、逐个执行、 汇报成败1。这行里多年的王者是 Jest;这本书选 Vitest,理由很实际: 它和 Vite 共用同一套转换管道,Jest 通常要另配的 ESM/TypeScript 支持直接消失, 各家公开的基准测试(统一条件下的计时对比)普遍显示快 3–5 倍,而且 API 和 Jest 基本兼容——在这学到的模式, 进了 Jest 项目照样用2。
规矩两条:文件名必须含 test 或 spec 才会被认出来(惯例:test 表示单元测试,
spec 表示集成测试)3;测试文件一般和被测文件同名、同目录4。
最小的测试长这样:
import { expect, test } from "vitest";
function areYouOldEnoughToDrive(age: number) { return age >= 18; }
test("Check whether person is old enough to drive", () => {
expect(areYouOldEnoughToDrive(33)).toBe(true);
});
test 是容器(第一个参数是说明文字,第二个是测试体),expect 做断言;
toBe 这类比对函数叫 matcher(匹配器),expect(x).not.toBeFalsy()
里的 not 是把任意匹配器取反5。跑起来后 Vitest 不会退出,而是守着文件:
改测试存盘就自动重跑,且只重跑受影响的那批6。配套的容器还有
describe(把相关测试分组)、beforeAll/beforeEach(测试前的一次/
每次准备)、afterAll/afterEach(收尾)7。
2. React Testing Library:像用户一样查屏幕¶
组件测试需要一个「浏览器」——但没有。Vitest 默认跑在 Node 环境里, 测不了 Web 应用,所以要切到 jsdom 模式:一个用 JavaScript 模拟的浏览器 环境8。配置就三行:
test: {
globals: true, // 让 expect 等 API 全局可用(Testing Library 假设如此)
environment: "jsdom", // 模拟浏览器
setupFiles: "./src/setupVitest.ts", // 全局 setup,省得每个文件手动引 jest-dom
}
React Testing Library(RTL) 的目标写在自己的宣言里: 让测试以用户使用页面的方式来做10。所以它的查询全是「用户视角」的—— 按文字、按角色、按标签——而不是按 CSS 类名或组件内部结构。最小组件测试:
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
render 把组件「渲染」进虚拟 DOM,并返回一组查询函数;
getByText 按文本找元素(这里用正则 /learn react/i 表示不分大小写);
toBeInTheDocument 断言它真的在渲染结果里——这个匹配器来自
@testing-library/jest-dom 包,不是 Vitest 自带11。
要点:getBy* 找不到元素时直接让测试失败,不存在返回 null 让你忘了查12。
3. Mock 四层:替身各有各的地盘¶
被测代码常常连着「不想真调的东西」——真的发网络请求、真的依赖某个模块。 mock 就是覆盖机制:把真的实现换成测试专用的替身13。Vitest 给了四层, 由小到大的管辖范围:
| 工具 | 管什么 | 特点 |
|---|---|---|
vi.fn |
单个函数 | 直接造一个假函数,可指定返回;.mockImplementationOnce 能让第一次、第二次调用各回各的值;能断言「被调了几次」(toHaveBeenCalledTimes)14 |
vi.mock("./Api") |
整个模块,整个文件 | 文件级生效(调用会被隐形提升到文件顶部);只给路径不给实现时,所有导出变成返回 undefined 的空壳(automock);加 { spy: true } 则保留原实现、只包一层间谍15 |
vi.doMock |
整个模块,单个测试 | 不提升、只在本测试内生效;配合 vi.importActual 拿到真模块,展开后再覆盖个别成员——partial mock(部分 mock)16 |
vi.spyOn(模块, "函数名") |
模块内部被调用的函数 | 替换实现的同时保留「被谁调过」的记录;测完 spy.mockRestore() 复位,免得污染别的测试17 |
主线例子的场景:DisplayTodos 组件——输入用户名、点按钮、
Suspense 里渲染 UserTodos(用 use 等一个 getUsersTodos 返回的 Promise)。
测试要验证「点按钮后列表出现了」,但绝不能真发网络请求,于是:
const spy = vi.spyOn(ApiModule, "getUsersTodos")
.mockImplementation(async (username) => [
{ id: 1, userId: 1, username, title: "test@test.com" },
]);
然后模拟用户动作:userEvent.setup() 初始化伪浏览器;
user.type(screen.getByTestId("user-input"), "bret") 找到输入框打字;
user.click(screen.getByText("Show Message & Data")) 点按钮;
await screen.findByRole("list") 等列表出现——注意 findBy* 是异步的,
会一直等到元素出现为止(get 系列是同步的,现在没有就是失败)18。
最后 expect(spy).toBeCalled() 确认替身真的被调用过17。
4. 主线例子:一场由并发规则引发的真实测试事故¶
这个测试第一次跑是失败的——而且是最让人无助的那种:测试在虚拟 DOM 里跑,
你根本「看不见」界面发生了什么19。书里开了 Vitest 的 --ui 模式
(图形界面,能在左侧看到每个测试渲染出的实际 HTML),才发现:
列表根本没有渲染出来,findByRole("list") 永远等不到东西20。
根因在第 06 章埋着:和 Suspense 关联的组件,放进 state 时必须包在
startTransition 里。原代码直接 setTodoControl(<UserTodos .../>),
违反并发规则,子组件渲染不出来;改成:
测试就绿了21。这个例子值得单独记住,因为它展示了这类事故的标准排查路径: 测试失败 → 开 UI 模式看渲染结果 → 把现象对照回框架规则。 「写得出测试」和「修得好测试」之间,隔的就是这一步。
5. Chrome DevTools:前端的主刀器械¶
Chrome 内置的开发者工具(DevTools)是许多前端开发排查问题的主力22: Elements 页签看和改 HTML/CSS(改样式即时预览,适合快速试验); Console 看日志;Sources 打断点逐行执行;Network 监视浏览器 发出的每个请求。再往外还有:Performance(性能分析,慢渲染、布局抖动)、 Lighthouse(性能/无障碍/SEO 自动化审计)、Device Mode(模拟手机视口与触摸)、 Application(查 cookie、localStorage、service worker)、内存分析(找泄漏)23。 这一章没逐页展开——定位是「让你知道桌面上有哪些刀」,日常手艺在实战中磨。
6. 作者的判断与证据¶
- 「Vitest 优于 Jest 的具体理由」(共享转换管道、免配置、3–5 倍快、API 兼容) 是明确的选型论证2;「3–5 倍」来自常见基准测试,是转述。
- 「像用户一样测,不测实现细节」是 RTL 的官方目标,作者把它作为方法论立场 贯穿全章10;其推论「重构底层实现时测试不碎」是该方法的核心理由。
- 「mock 难,建议把例子多跑几遍变体」是作者对这部分难度的坦率承认24。
7. 边界与局限¶
- 不测实现细节的另一面:纯逻辑(工具函数、reducer)的测试本章只给了最简例子;
组件测试全篇靠
data-testid撑场——严格说 testid 已经偏向「实现细节」, RTL 的纯血统做法更鼓励按角色/文字查询。 - 没有讲测试策略:多少测试算够、测什么不值得测、覆盖率怎么用, 本章一字未提;「vitest --ui 的安装与使用」着墨反而更多。
- 异步测试的通用模式(waitFor、findBy 的底层)只演示了 findBy 一族。
- Chrome DevTools 一节是导览式的,不构成可操作的调试手册。
判断(我们的,不是书里的): 把「mock 按管辖范围分四层」教, 比按 API 字母表教有效得多——真实困惑从来不是「vi.fn 怎么写」, 而是「我该 mock 模块还是函数」。主线例子(失败 → UI 模式 → 发现是 startTransition 规则)是本章最值钱的一段:它把第 06 章一条抽象规则 变成了一次可复现的排障经历,这比十条「注意事项」都记得牢。 如果错,会错在: 以「打字→点按钮→等列表」为原型的交互测试, 对以渲染展示为主的组件(图表、编辑器)覆盖不好;本书应用(第 15 章)的 测试也主要沿这条路径走,读者可能误以为交互测试就是组件测试的全部。
8. 可带走的¶
- Vitest = 跑测试;选它是因为和 Vite 同管道、免配置、快 3–5 倍、API 兼容 Jest。
- 文件名含 test/spec 才被识别;
test容器 +expect断言 + matcher 比对,not取反。 - 组件测试切 jsdom 环境;RTL 按用户视角查屏幕:
getByText找不到即失败。 - mock 四层:vi.fn(单函数)→ vi.mock(整模块,提升)→ vi.doMock(单测试内, 配合 importActual 做部分 mock)→ vi.spyOn(模块内部函数,记得 mockRestore)。
userEvent模拟打字点击;findBy*异步等待元素出现,getBy*立即失败。- Suspense 组件进 state 要包 startTransition——测试失败可能是框架规则没遵守,
用
--ui模式看渲染结果来定位。 - DevTools 的六个常用面板:Elements/Console/Sources/Network,加 Performance、 Lighthouse、Application。
9. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| Vitest 的选型理由 | Chapter 7: Testing with Vitest and React Testing Library | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:137(搜「built on top of Vite」) · text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:139(搜「3–5x」) |
| 文件命名规矩 | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:159(搜「in the name of the file or they will be 」) |
| test/expect/matcher | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:193(搜「function is used to do assertions and th」) · text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:657(搜「Matchers」) |
| 守护模式只重跑受影响的 | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:315(搜「only the affected」) |
| describe 与 beforeAll | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:535(搜「descriptor」) · text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:573(搜「runs once before all」) |
| jsdom 环境 | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:827(搜「simulates a browser」) |
| RTL 的用户视角目标 | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:803(搜「like how a user」) |
| getByText 与 toBeInTheDocument | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:999(搜「/learn react/i」) · text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:1067(搜「not part of Vitest」) |
| vi.fn 与 mockImplementationOnce | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:1356(搜「vi.fn」) · text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:1540(搜「mockImplementationOnce」) |
| vi.mock 提升与 automock | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:1696(搜「invisibly hoisted」) · text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:1718(搜「automock」) |
| partial mock 与 importActual | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:1838(搜「partial mock」) · text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:1848(搜「importActual」) |
| vi.spyOn 与 mockRestore | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2657(搜「.spyOn(ApiModule, "getUsersTodos")」) · text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2772(搜「mockRestore」) |
| userEvent 与 findBy 异步 | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2628(搜「simulate user-initiated」) · text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2764(搜「will wait until」) |
| 测试失败与 UI 模式排障 | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2786(搜「cannot actually see the UI」) · text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2802(搜「--ui」) |
| startTransition 修复 | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2856(搜「Action Hook」) · text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2888(搜「startTransition(async」) |
| DevTools 面板导览 | 同上 | text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2910(搜「developer tools」) · text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2914(搜「Performance panel」) |
-
出处:「Chapter 7: Testing with Vitest and React Testing Library」第 127–131 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:131,搜「command line apps」)。 ↩ -
出处:同上第 133–141 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:139,搜「3–5x」;:141,搜「API compatible」)。 ↩↩ -
出处:同上第 151–169 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:151,搜「must have」)。 ↩ -
出处:同上第 504–512 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:512,搜「standard practice」)。 ↩ -
出处:同上第 189–197 段与 651–781 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:193,搜「function is used to do assertions and th」;:657,搜「Matchers」;:769,搜「not Modifier」)。 ↩ -
出处:同上第 307–315 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:307,搜「does not immediately exit」;:315,搜「only the affected」)。 ↩ -
出处:同上第 531–599 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:535,搜「descriptor」;:573,搜「runs once」;:587,搜「beforeEvery」)。 ↩ -
出处:同上第 815–828 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:823,搜「jsdom」;:827,搜「simulates a browser」)。 ↩ -
出处:同上第 1127–1157 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:1135,搜「no longer need to import it into every test」;:1146,搜「setupFiles」)。 ↩ -
出处:同上第 795–805 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:803,搜「like how a user」)。 ↩↩ -
出处:同上第 937–1067 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:964,搜「renders learn react link」;:1063,搜「not part of Vitest」)。 ↩ -
出处:同上第 1015–1027 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:1027,搜「fail your test」)。 ↩ -
出处:同上第 1682–1685 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:1682,搜「override mechanism」)。 ↩ -
出处:同上第 1354–1378 段与 1506–1656 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:1360,搜「mock a single function」;:1542,搜「mockImplementationOnce」;:1648,搜「toHaveBeenCalledTimes」)。 ↩ -
出处:同上第 1672–1728 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:1696,搜「invisibly hoisted」;:1716,搜「automock」;:1726,搜「spy: true」)。 ↩ -
出处:同上第 1836–1992 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:1838,搜「partial mock」;:1876,搜「only within a specific container」;:1896,搜「importActual」)。 ↩ -
出处:同上第 2650–2776 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2657,搜「.spyOn(ApiModule, "getUsersTodos")」;:2688,搜「replace the original implementation」;:2772,搜「mockRestore」)。 ↩↩ -
出处:同上第 2614–2765 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2628,搜「simulate user-initiated」;:2764,搜「will wait until the desired element appears」)。 ↩ -
出处:同上第 2780–2791 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2786,搜「cannot actually see the UI」)。 ↩ -
出处:同上第 2792–2846 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2802,搜「--ui」;:2846,搜「looking for an unordered list」)。 ↩ -
出处:同上第 2848–2896 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2856,搜「Action Hook」;:2888,搜「startTransition(async」)。 ↩ -
出处:同上第 2908–2921 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2920,搜「primary tool」)。 ↩ -
出处:同上第 2910–2918 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2914,搜「Performance panel」)。 ↩ -
出处:同上第 2902–2906 段(
text/42-ch07-chapter-7-testing-with-vitest-and-react-testing-.txt:2906,搜「trying different permutations」)。 ↩