大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
E2E 与 Playwright
这一课你将能做到
- 配置 Playwright 双服务环境(Next.js 前端 + Spring Boot 后端),写出「打开页 → 可见 → 跳转」的关键路径冒烟
- 用
getByRole/ 自动等待 /waitForResponse替代 sleep,把 flaky 压到最低 - 列出本站 3~5 条值得 E2E 守护的路径,并说清哪些不该进 E2E
为什么要学
单元与 MockMvc 再绿,仍可能出事:前端路由配错、环境变量 NEXT_PUBLIC_API_BASE 指错、CORS 没开、真浏览器才暴露的水合错误。人话:E2E 是用真实浏览器走用户的路——少、慢、贵,但只有它能抓住「整条链断了」。
它不能替代金字塔底部。本课目标是会写稳定的冒烟,并知道 CI 策略:PR 上仍以 mvn test + npm test 为主,E2E 放主分支或发布前跑(分轨思想见 test-ci)。
CodeForge 值得 E2E 守护的命门很清楚:访客能打开首页、能看到课程列表、注册登录能走通、提交代码能看到判题状态。每多一条用例就要多付一份维护税——文案改、路由改、数据改都会红,所以清单要像产品的命门列表,不是测试同学的 KPI。
先建立图像
E2E: Browser ──► platform(Next.js :3000) ──HTTP──► backend(Spring Boot :8080)
▲ │
└── Playwright 驱动:点击、填表、断言 ←──────┘
MySQL / Redis
成本:分钟级;失败定位面横跨四层
策略:3~5 条命门;能下沉到组件测 / MockMvc 的就下沉
| 层级 | 已被谁测 | E2E 要不要重复 |
|---|---|---|
| 业务分支 | Mockito 单测 | 不重复 |
| HTTP 契约 | MockMvc | 不重复字段细节 |
| 整页用户路径 | — | E2E 主场 |
选择器优先级与 Testing Library 一致:role / label / 文本 ≫ CSS class / XPath。
跟着做
步骤 1:安装与最小配置
cd platform
npm install -D @playwright/test
npx playwright install chromium
playwright.config.ts——重点是 webServer 让脚本自动拉起前后端:
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry", // 失败重试时录 trace,定位神器
},
retries: process.env.CI ? 1 : 0,
webServer: [
{
command: "cd ../backend && mvn spring-boot:run",
url: "http://localhost:8080/tracks",
reuseExistingServer: true,
timeout: 120_000,
},
{
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: true,
timeout: 120_000,
},
],
});
reuseExistingServer: true 让你本地已手动起好的服务不被重复拉起;CI 里它会自己起全套。
步骤 2:第一条冒烟——访客视角
e2e/smoke.spec.ts:
import { test, expect } from "@playwright/test";
test("首页可见并能看到课程入口", async ({ page }) => {
await page.goto("/");
// 等待可见性,而不是 sleep
await expect(
page.getByRole("heading", { level: 1 })
).toBeVisible();
await page.getByRole("link", { name: /课程|tracks/i }).first().click();
await expect(page).toHaveURL(/tracks/);
});
npx playwright test
应看到:浏览器自动操作并通过。失败时看 test-results/ 里的 trace 与截图——先归因再改代码(步骤 6)。
步骤 3:课程详情路径
test("打开指定 track 可见课时标题", async ({ page }) => {
await page.goto("/tracks/testing-essentials");
await expect(
page.getByRole("heading", { name: /测试/i })
).toBeVisible();
});
slug 用仓库里确定存在的数据(种子或迁移产生的),绝不依赖某人手工点过的脏库。
步骤 4:判题路径——等网络而不是猜时序
「提交代码 → 出现排队中徽章」涉及跨端请求,用 waitForResponse 精确同步:
test("提交代码后出现判题状态徽章", async ({ page }) => {
// 前置:已在测试环境登录(按本站真实登录表单填写,或用测试后门账号)
await page.goto("/tracks/testing-essentials/lessons/test-intro");
const apiDone = page.waitForResponse(
(res) => res.url().includes("/submissions") && res.ok()
);
await page
.getByRole("textbox", { name: /代码/i })
.fill("class Main { public static void main(String[] a) {} }");
await page.getByRole("button", { name: /提交/ }).click();
await apiDone;
await expect(page.getByText(/排队中/)).toBeVisible();
});
网络慢时固定 waitForTimeout(3000) 会随机红;期望断言(expect(...).toBeVisible())自带重试轮询,事件等待(waitForResponse)精确到协议层——两种手段覆盖 95% 的时序问题。
步骤 5:划定「只守这些」的清单
在 platform/e2e/README.md 里写死:
必跑冒烟:
1. 首页可打开(h1 可见)
2. /tracks 列表有课程入口
3. /tracks/<已知slug> 标题可见
4. 登录后能进受保护页面
5. 提交代码后判题徽章出现
明确不测:主题色、间距、hover 效果、每个按钮文案。
新增 E2E 前先改这张清单:说不出「命门」理由的,下沉到组件测或 MockMvc。
步骤 6:失败时的分层归因三问
E2E 一红,先别改业务代码,按顺序问:
- 环境? backend 起了吗、
NEXT_PUBLIC_API_BASE指对了吗、种子数据在吗? - 选择器? 文案/结构是不是刚改过、role 还能定位吗?
- 真回归? 底层 MockMvc / 单测是否已红——底层红先修底层。
把这三问写成团队习惯,E2E 才不会变成「吓人但不解决问题」的红灯。
讲清楚要点
- E2E 失败面大,所以用例必须短、路径必须关键:一条用例走一个用户故事,失败时才有机会快速归因。
- 环境是变量,基址/账号/种子必须进配置与文档:
baseURL、webServer、种子 slug 全部落文件,禁止口口相传。 - 自动等待是稳定性核心:
expect自带重试、waitForResponse精确同步,固定 sleep 是 flaky 之源。 - 选择器用 role/文本:与组件测同一哲学——测用户感知,不绑实现细节。
- CI 分级:PR 跑快测,E2E 挂主分支/夜间/发布前——分钟数是钱,队列是士气。
- 金字塔顶部宜窄:每加一条 E2E 先问「能否下沉到 MockMvc / 组件测」,能沉就沉。
你可能会懵的地方
-
本地绿、CI 红是为什么? 常见:CI 没起 backend、baseURL 环境变量不同、浏览器依赖没装、CI 机器更慢放大了时序问题。先看 trace artifact 与两个服务的启动日志,再谈重试。
-
要不要测真实验证码 / OAuth 跳转? 真第三方易脆。测试环境用后门验证码开关、mock IdP,或只测「已带登录 Cookie 的页面」。第三方对接本身留给手动验收或专项集成测。
-
Playwright 和 Cypress 选哪个? 都能做 E2E。Playwright 多浏览器、自动等待成熟、
webServer配置对双端项目友好——本课用它;思维要点在两者间通用:少而稳、抗 flaky。 -
一条 E2E 里断言二十个字段? 过度耦合。E2E 抽「关键可见性」;字段细节是 MockMvc 契约测的职责。
-
E2E 要不要跑每个 PR? 默认不。快测挡 PR;E2E 挂主分支 push / nightly / 发布前。若某条 E2E 稳定且快到可以挡 PR,先想想它是不是该下沉。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 用 E2E 替代所有单测 | 慢、难定位 | 底多顶少 |
waitForTimeout 铺满 | 随机 flaky | 自动等待 / 事件等待 |
| 选择器绑随机 class | 样式一改全红 | getByRole |
| 依赖手工数据库状态 | 不可重复 | 种子数据 / 已知 slug |
| PR 全量 E2E 无分轨 | 队列爆炸 | PR 快测 + E2E 分级 |
卡点急救
webServer起不来或超时:url必须是「服务就绪后返回 2xx/3xx 的地址」,backend 用一个确定存在的 GET 路由;timeout 调到 120s 以上(首次 mvn 拉依赖很慢)。- strict mode violation(多个元素匹配):
getByRole命中多个时用.first()、getByRole(..., { exact: true })或收窄父容器;别用page.locator("css=...").nth(3)这类脆定位。 - 本地绿 CI 红:对比两端日志确认服务都起了;把 trace 打开(
trace: "on-first-retry"+ CIretries: 1),下载 artifact 逐步回放。
面试常问
-
E2E 测试 flaky 怎么治理? 四板斧:选择器用 role/文本不绑样式;等待用自动重试断言与事件等待,不用固定 sleep;数据用种子与已知 slug,不依赖共享脏库;CI 分轨 + 失败重试一次 + trace 回放定位。目标是让「红」几乎只代表真回归。
-
为什么 E2E 少而关键,而不是多多益善? 每条 E2E 都有维护税:文案、路由、数据一变就红,且失败定位面横跨前后端与环境。金字塔要求顶部窄——把细节验证下沉到组件测和 MockMvc,E2E 只守「整条链是否通」。
-
E2E 在 CI 里应该放在什么频率? PR 只跑快测(
mvn test/npm test)保证分钟级反馈;E2E 挂主分支 push、夜间或发布前。若要求 PR 必跑 E2E,需配缓存与并行分片,否则流水线会成为交付瓶颈。
自测三问
- 为什么「测每个按钮颜色」不适合 E2E?
getByRole相比 CSS 选择器的两大好处是什么?waitForResponse与waitForTimeout的本质区别?
小结一句话
Playwright 守用户路径,少而稳;日常开发靠 JUnit 与 Vitest,发布前用 E2E 补最后一道门。
延伸
- Playwright · Writing tests
- Playwright · Best Practices
- 下一课:test-coverage 用覆盖率报告找测试盲区,并设计不被数字绑架的质量门禁。