页面报错,到底是前端还是后端背锅?这篇用 Playwright 的
request直连后端,带你从跑通第一条/health测试开始,一步步搞懂断言、异常输入、CRUD 。全程不用开浏览器,新手照着抄就能跑。
📌 先看这个:你能收获啥
- 搞懂 UI 测试 vs API 测试 差在哪、啥时候该直接打接口
- 跑通你的第一条 Playwright API 测试,并看懂每条断言在查什么
- 写出"故意传错也通过"的异常用例,再也不怕看到 422
- 改出一个自带清理的 CRUD 测试,单独跑也不留垃圾数据
- 拿到一份发布前自查清单,别把 token、私人数据误提交
🗺️ 目录
- [UI 测试和 API 测试,差在哪](#UI 测试和 API 测试,差在哪)
- 认识现有文件
- 环境搞起来,跑通第一条测试
- 逐行读最简单的用例
- 从状态码进阶到返回内容
- [正常测试 vs 异常测试](#正常测试 vs 异常测试)
- 跟着项目加一条测试
- [搞懂 Recurring CRUD](#搞懂 Recurring CRUD)
- 挂了怎么排查
- 练习顺序
- [附:发布前自查 & 下篇预告](#附:发布前自查 & 下篇预告)
1️⃣ UI 测试和 API 测试的区别
💡 一句话记住 :UI 测试点页面,API 测试直接打后端;普通接口用例根本不用开浏览器。
UI 测试是让浏览器去点页面;API 测试 用 Playwright 的 request 直接请求后端,再用 expect 查返回。
本项目两个地址用途不一样,别搞混:
| 地址 | 用途 |
|---|---|
http://localhost:5173 |
前端页面,给 UI 测试 用 |
http://localhost:5001 |
后端 API,给本文测试用 |
比如 request.get('/health') 在 API 项目里实际访问 http://localhost:5001/health。
为啥要测 API? 页面显示异常,锅可能在前端也可能在后端。单独验证后端能不能正确处理请求,相当于多一份"接口层证据",定位问题快得多。
先记几个词(新手向):
| 词 | 怎么理解 |
|---|---|
| 请求 | 告诉后端"我要查/操作啥" |
| 响应 | 后端返回的状态码 + 数据 |
| GET / POST / PUT / DELETE | 查 / 提交(本项目用于创建·下载) / 更新(改名) / 删除类(解绑) |
| 断言 | 自动判断"实际结果符不符合预期"的语句 |
| 200 / 404 / 422 | 成功 / 记录不存在 / 输入校验不通过 |
🖼️ 【项目结构 + API 配置】
截编辑器里 4 个手写测试文件 +
playwright.e2e.config.ts的api配置段;框出testDir、testMatch、baseURL三处。

2️⃣ 文件目录
| 文件 | 内容 | 学啥 |
|---|---|---|
tests/api/health.api.spec.ts |
健康检查、OpenAPI、当前用户日志、Jobs 汇总 | GET、JSON、基础断言 |
tests/api/auth.api.spec.ts |
认证提供方、当前用户 | 状态码 + 用户字段断言 |
tests/api/job.api.spec.ts |
汇总、详情、不存在的 Job、非法下载参数 | 正常/异常输入、返回结构 |
tests/api/recurring.api.spec.ts |
查询、字段校验、完整 CRUD、清理 | POST/PUT/DELETE、数据生命周期 |
配置入口是根目录 playwright.e2e.config.ts,api 项目这样配:
ts
{
name: 'api',
testDir: './tests/api',
testMatch: '**/*.api.spec.ts',
use: { baseURL: 'http://localhost:5001' },
}
当前设置:单 worker、关完全并行、失败不重试;api 项目不依赖 UI 项目,所以单独选它不会先跑上传/页面清理。
🔑 划重点 :按业务拆文件 = 查认证问题直接看
auth.api.spec.ts;选api项目 = 请求走 5001。单 worker 让执行顺序好观察、减少共享数据冲突,但它替代不了测试数据隔离------只是当前设计的取舍,不代表 API 测试都必须串行。
3️⃣ 跑通第一条测试
3.1 先确认测试能被发现
powershell
npx playwright test --config=playwright.e2e.config.ts --project=api --list
--list 只收集用例、不发请求;能列出来 ≠ 后端或断言已通过。

3.2 只跑健康检查
powershell
npx playwright test tests/api/health.api.spec.ts --config=playwright.e2e.config.ts --project=api -g 'GET /health returns 200'

命令拆解:
| 部分 | 作用 |
|---|---|
npx playwright test |
调本项目 Playwright |
tests/api/health.api.spec.ts |
只选这个文件 |
--config=... |
指定用哪份配置 |
--project=api |
选 API 项目 |
-g '...' |
按用例名筛,只跑匹配的 |
通过后逐步扩大代码范围:
powershell
# 整个健康检查文件
npx playwright test tests/api/health.api.spec.ts --config=playwright.e2e.config.ts --project=api
# Jobs 测试
npx playwright test tests/api/job.api.spec.ts --config=playwright.e2e.config.ts --project=api
# 全部手写 API 测试(含会写数据的 CRUD)
npx playwright test tests/api/health.api.spec.ts tests/api/auth.api.spec.ts tests/api/job.api.spec.ts tests/api/recurring.api.spec.ts --config=playwright.e2e.config.ts --project=api
# 看最近一次 HTML 报告
npx playwright show-report
4️⃣ 最简单的用例
health.api.spec.ts 真例:
ts
import { test, expect } from '@playwright/test';
test('GET /health returns 200', async ({ request }) => {
const response = await request.get('/health');
expect(response.status()).toBe(200);
});
test定义用例,第一个参数是报告里显示的名async ({ request })拿到 HTTP 请求对象await等请求完成、拿到响应再继续response.status()读 HTTP 状态码expect(...).toBe(200)是断言;不是 200 用例就挂
5️⃣ 从状态码进阶到返回内容
auth.api.spec.ts 不只查 200,还查用户:
ts
const response = await request.get('/api/auth/me');
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.authenticated).toBe(true);
expect(body.user.user_id).toBe('dev:1');
response.json() 把响应转成 JS 对象。注意:这条要求当前认证用户恰好是 dev:1。
Jobs 汇总进一步查结构:
ts
const response = await request.get('/api/jobs/explorer/summary');
expect(response.status()).toBe(200);
const body = await response.json();
expect(body).toHaveProperty('user_id');
expect(Array.isArray(body.jobs)).toBe(true);
for (const job of body.jobs) {
expect(job).toHaveProperty('job_id');
expect(job).toHaveProperty('name');
expect(job).toHaveProperty('requested');
expect(typeof job.requested.cpu_cores).toBe('number');
}
6️⃣ 正常测试 vs 异常测试
预期来自当前测试代码;改接口时还要对照真实 API 规范和业务要求。
| 场景 | 请求 | 预期 |
|---|---|---|
| 服务健康 | GET /health |
200 |
| 已有 Job 详情 | GET /api/jobs/job_1 |
200(依赖 job_1 存在且可访问) |
| 不存在的 Job | GET /api/jobs/nonexistent-playwright-job-999999 |
404 |
| 批量下载参数类型错 | POST /api/jobs/download/multiple,job_ids 传字符串 |
422 |
| 创建周期任务缺名称 | POST /api/recurring/jobs,只传 user_id |
422 |
| 创建周期任务名称为空 | rname: '' |
422 |
| 改名缺必填 | PUT /api/recurring/jobs/rename,只传新名称 |
422 |
| 解绑缺必填 | DELETE /api/recurring/jobs/unbind,只传用户 |
422 |
异常测试不是"让测试报错",而是验证后端正确拒绝非法请求:
ts
const response = await request.post('/api/jobs/download/multiple', {
data: { job_ids: 'invalid-type' },
});
expect(response.status()).toBe(422);
data 是请求体。本例故意传错类型,收到 422 才算过。别看到 422 就当失败,也别为了过把 200/400/404/422 全加进允许范围。


7️⃣ Recurring CRUD
打开 recurring.api.spec.ts 的 Recurring Job CRUD lifecycle。CRUD = 增/查/改/删;本项目最后一步实际调的是 unbind(解绑),别据此推断数据库物理删除。
| 步骤 | 请求与请求体 |
|---|---|
| 创建 | POST /api/recurring/jobs:rname、user_id:'dev:1'、assoc_job_ids:[] |
| 读取 | GET /api/recurring/alljobs,查新名存在 |
| 改名 | PUT /api/recurring/jobs/rename:old_rname、new_rname、user_id |
| 再读取 | 查新名在、旧名不在 |
| 解绑 | DELETE /api/recurring/jobs/unbind:recurring_name、user_id |
| 最后检查 | 再查列表,确认改名后名称不存在 |
测试用 pw-test-${Date.now()} 作名称,把几步放同一条用例里,自己管数据,不依赖别的测试先跑。
两个状态变量:created 标记是否还需清理,currentName 存当前应清理的名称。finally 会在中途断言失败时仍尝试解绑,降低数据残留概率。

当前实现的边界(写测试要心里有数):
- 存在性检查用
JSON.stringify(body).includes(...)搜名称,没按明确响应字段+用户精确匹配;确认真实结构后可改字段比较 - 清理响应失败只输出警告,仍要看日志、查残留;不保证所有失败都清理成功
- 此用例用
pw-test-前缀,不能假设 UI 的playwright-recurring-*清理逻辑会管这些数据;API 项目也不依赖 UI cleanup
运行这条:
powershell
npx playwright test tests/api/recurring.api.spec.ts --config=playwright.e2e.config.ts --project=api -g 'Recurring Job CRUD lifecycle'

9️⃣ 挂了怎么排查
| 现象 | 优先查 |
|---|---|
ECONNREFUSED、连不上 |
后端跑没、5001 暴露没、baseURL 对没 |
| 打到 5173,或 JSON 解析遇到 HTML | 漏了 --config=... --project=api |
/api/auth/me 不是 dev:1 或 401/403 |
后端认证环境是否符合前提;API 配置无显式登录 |
job_1 返 404 |
测试库是否存在且当前用户可访问 |
| 预期 422 实际 200 | 对照规范查字段校验;确认是否意外建数据需清理 |
| CRUD 后列表仍有记录 | 查解绑响应、当前名称、用户 ID、清理警告 |
| 新测试没被发现 | 文件路径、.api.spec.ts 后缀、项目选择、-g 过滤 |
| 同一 API 测试出现三个浏览器项目 | 用了默认配置,改用本文 API 命令 |
ts
console.log('Status:', response.status());
console.log('Body:', await response.text());
expect(response.status()).toBe(200);


🔟 本章内容
- 跑通
/health,理解请求地址和状态码 - 读
/openapi.json用例,理解 JSON 和字段断言 - 跑 Auth 和 Jobs,识别
dev:1、job_1的环境依赖 - 读非法下载参数用例,理解"预期拒绝也是成功"
完成后,你应能解释:每条用例请求了啥、为啥预期这结果、依赖哪些数据、失败怎么清理排查。
尝试一下其他风格来写blog,如果流量低,就还是返回到老格式
下篇预告
这个完结后,准备做AI-tesintg,用qwen api,嘿嘿