使用 Vitest、Testing Library 和 jsdom,我们可以快速验证函数与 React 组件。但 jsdom 终究不是真实浏览器:它不会完整还原浏览器导航、页面布局、跨页面流程和整个前后端链路。
这篇文章从组件测试的边界出发,解释 Playwright 为什么存在、Locator 如何工作、自动等待解决了什么问题,以及怎样合理分配单元测试、组件测试和端到端测试。
一、jsdom 能证明什么,不能证明什么
组件测试的结构通常是:
Vitest
↓
Node.js
↓
jsdom 模拟 document 和 window
↓
React 组件它擅长验证:
- 点击按钮后状态是否变化;
- 表单输入与错误提示;
- 条件渲染;
- 异步数据返回后的界面;
- 组件是否调用了正确依赖。
但它不能完整证明:
- 网站能否在真实浏览器中打开;
- 页面导航与 URL 是否正确;
- 浏览器是否真正加载了前端资源;
- 前端、后端和数据库能否贯通;
- 元素是否被遮挡或真的可以点击;
- Chromium、Firefox 与 WebKit 中的关键流程是否一致。
这正是端到端测试的任务。
二、Playwright 在系统中的位置
Playwright Test 会启动真实浏览器并模拟用户操作:
Playwright 测试程序
↓
Chromium / Firefox / WebKit
↓
访问正在运行的网站
↓
前端 JavaScript 执行
↓
调用后端接口
↓
读取或写入数据
↓
最终结果返回页面因此“端到端”的两端是:
用户发起操作的一端
↕
完整系统
↕
用户看到结果的一端一个最小测试:
import { test, expect } from "@playwright/test";
test("文章页显示阅读时间", async ({ page }) => {
await page.goto("/articles/testing");
await expect(
page.getByText("预计阅读 2 分钟")
).toBeVisible();
});这里不是把某个组件挂载进 jsdom,而是真正让浏览器访问网页。
三、Locator 是动态查找说明,不是静态元素
const button = page.getByRole("button", {
name: "创建文章"
});这行代码创建的是 Locator。它可以理解为一份说明:
在当前页面中,找到角色为 button、名称为“创建文章”的元素。创建 Locator 时并不需要页面中已经存在该按钮,也没有把某个 DOM 节点永久保存下来。真正执行操作时:
await button.click();Playwright 才会根据最新 DOM 查找目标元素。
这对 React 等会重新渲染页面的框架尤其重要:即使旧按钮被替换,Locator 仍会在下一次操作时找到新的匹配元素。
getByRole() → 创建查找说明
click() → 查找最新元素并执行浏览器操作因此,创建 Locator 通常不需要 await,动作和异步断言才需要。
四、自动等待不是固定休眠
执行:
await button.click();Playwright 会在点击前自动检查目标是否满足必要条件,包括:
只匹配一个元素
↓
元素可见
↓
位置稳定
↓
没有被其他元素挡住
↓
能够接收事件
↓
没有被禁用
↓
执行点击如果条件暂时不满足,它会等待;如果直到超时仍无法满足,才报告错误。
不推荐这样写:
await page.waitForTimeout(3000);
await button.click();固定休眠的问题是:
- 页面 100ms 就准备好,却浪费 3 秒;
- 页面需要 4 秒,等待 3 秒仍会失败;
- 运行环境速度变化时容易产生偶发失败。
更可靠的原则是:
等待可观察条件成立,而不是等待猜测的时间经过。Playwright 的异步断言也会自动重试:
await expect(
page.getByRole("heading", { name: "测试入门" })
).toBeVisible();五、完整文章创建流程
import { test, expect } from "@playwright/test";
test("用户可以创建文章", async ({ page }) => {
await page.goto("/articles/new");
await page
.getByLabel("文章标题")
.fill("测试入门");
await page
.getByRole("button", { name: "创建文章" })
.click();
await expect(
page.getByRole("heading", { name: "测试入门" })
).toBeVisible();
});这段测试贯穿了多个系统边界:
打开页面
↓
React 渲染表单
↓
用户输入标题
↓
提交接口请求
↓
后端验证并保存
↓
跳转文章详情页
↓
浏览器显示新文章标题它提供的信心很高,但失败原因也可能位于任何一层,所以不能用 E2E 取代所有低层测试。
六、为什么优先使用用户可见定位
推荐:
page.getByLabel("文章标题");
page.getByRole("button", { name: "创建文章" });
page.getByRole("heading", { name: "测试入门" });谨慎使用:
page.locator("#title");
page.locator(".submit-button");
page.locator("form > div:nth-child(2) > button");CSS 类名与 DOM 层级是实现细节。把 .submit-button 改成 .primary-action 并没有改变用户功能,测试不应该因此失败。
按角色、标签和可见名称查找,更接近用户与辅助技术感知页面的方式,也能让测试在合理重构后保持稳定。
七、三层测试分别回答什么问题
仍以阅读时间为例:
| 测试层级 | 主要问题 | 常用工具 |
|---|---|---|
| 单元测试 | calculateReadingTime(201) 是否返回 2? | Vitest |
| 组件测试 | React 是否显示“预计阅读 2 分钟”? | Vitest + Testing Library + jsdom |
| E2E 测试 | 用户打开真实文章页后能否看到阅读时间? | Playwright |
单元测试详细覆盖规则
test.each([
[0, 1],
[199, 1],
[200, 1],
[201, 2],
[400, 2]
])("%i 个单词返回 %i 分钟", (words, expected) => {
expect(calculateReadingTime(words)).toBe(expected);
});组件测试验证界面连接
render(<ReadingTime words={201} />);
expect(
screen.getByText("预计阅读 2 分钟")
).not.toBeNull();E2E 验证关键流程贯通
await page.goto("/articles/testing");
await expect(
page.getByText("预计阅读 2 分钟")
).toBeVisible();没有必要在三个层级都重复测试 0、199、200、201 和 400。边界规则属于单元层;组件层确认结果能连接到界面;E2E 层确认真实系统可以贯通。
八、为什么不能全部写成 E2E
如果阅读时间错误,单元测试会直接报告:
calculateReadingTime
预期 2,实际 1E2E 测试只能告诉我们:
页面没有显示“预计阅读 2 分钟”可能原因包括:
- 计算函数错误;
- React 组件错误;
- 接口请求失败;
- 后端返回错误;
- 测试数据不存在;
- 页面尚未加载;
- Locator 写错。
E2E 更接近真实用户,但运行更慢、维护成本更高、定位失败更困难。因此常见策略是:
大量快速单元测试
+
适量组件或集成测试
+
少量关键 E2E 测试重点不是机械追求某个比例,而是让每一层测试自己最擅长发现的问题。
九、异步代码中的 await
Playwright 测试程序运行在 Node 中,浏览器是另一个执行主体:
Node 测试代码
↓ 发送命令
浏览器导航、输入或点击
↓ 返回结果
Node 继续执行测试所以这些操作返回 Promise:
await page.goto(...);
await locator.fill(...);
await locator.click();
await expect(locator).toBeVisible();await 不会冻结整个 JavaScript 线程,而是暂停当前异步测试函数。Promise 完成后,后续代码恢复执行;Playwright Test 会等待测试函数返回的 Promise,直到它成功完成或抛出错误。
十、常见反模式
1. 使用固定等待
await page.waitForTimeout(5000);应改为等待具体页面条件或直接依赖 Locator 的自动等待。
2. 依赖脆弱 CSS 结构
page.locator("div:nth-child(3) > button");优先改为用户可见的角色、标签或名称。
3. 用 E2E 重复所有边界用例
详细算法边界留给单元测试,E2E 只保留关键成功和关键失败流程。
4. 为了让测试通过而强制点击
Playwright 允许在部分动作中使用 force 跳过某些检查,但如果按钮被遮挡,真实用户也可能无法点击。强制操作容易掩盖真实界面问题,应谨慎使用。
总结
Playwright 的价值不只是“能点浏览器”,而是提供了一套面向真实用户行为的测试模型:
Locator 描述用户要找的元素。
自动等待保证元素具备真实可操作性。
Web-first 断言等待可观察结果成立。
真实浏览器验证关键系统流程。最终的测试策略不是在 Vitest 与 Playwright 之间二选一,而是合理分工:
Vitest 证明局部规则。
Testing Library 证明组件行为。
Playwright 证明关键流程能够贯通。
没有回应