成为全栈·Next.js 网站前台篇·质量门禁:契约测试、会话竞态、双运行时与浏览器验收
类型检查、单元测试、接口联调、页面冒烟和浏览器操作证明的是不同事情。把它们统称为“测试通过”,会让没有覆盖的风险悄悄消失。
前言
本项目曾出现过一个典型问题:后端退出接口测试通过,浏览器里的退出按钮却失败。原因是直接接口工具携带了 access token,而真实客户端封装错误地跳过鉴权。
这个故障说明,测试层级不能互相替代。接口可用不等于组件调用正确,构建成功不等于 Worker 缓存正确,桌面截图也不等于键盘和手机路径可用。
每道门禁回答一个问题
| 门禁 | 能证明 | 不能证明 |
|---|---|---|
| 类型检查 | 调用与类型约束一致 | 运行行为正确 |
| Biome | 规范与静态问题符合规则 | 用户流程可用 |
| 单元测试 | 被覆盖的不变量成立 | 组件接线与真实 API 正确 |
| 接口联调 | 契约、代理和测试库往返 | 浏览器按钮使用了正确封装 |
| 页面冒烟 | 路由、metadata、状态码 | 所有交互和视觉正确 |
| 浏览器验收 | 指定用户路径实际可操作 | 全浏览器、全设备覆盖 |
| 双构建 | Node 与 Worker 产物可生成 | 线上资源、容量和缓存 SLA |
从共享 OpenAPI 重新生成类型
pnpmgen:apipnpmtypecheck类型来源是共享契约,不手写第二套 DTO。生成成功只能说明 schema 可转换,仍要用真实响应检查业务字段、错误码与消费者行为。
生成器与应用 TypeScript 版本曾存在兼容差异,因此工具运行环境独立;这类问题应与业务源码错误分开诊断。
并发会话必须用可控时序测试
test('parallel expired requests share one refresh',async()=>{constresults=awaitPromise.all([request('/me/profile'),request('/me/favorites'),request('/me/history'),])assert.equal(refreshes,1)assert.deepEqual(results,['private-data','private-data','private-data'])})test('logout during refresh cannot restore old account',async()=>{awaitassert.rejects(request('/me/profile'))assert.equal(useAuthStore.getState().accessToken,null)})这些测试保护刷新 Promise、会话代次和单次重放。只测“登录成功”看不到任何竞态风险。
测试真实客户端适配器,而不只测端点
test('logout sends backend-required access token',async()=>{awaitlogout()assert.equal(sentAuthorization,'Bearer logout-token')})这个用例来自实际浏览器故障。它把覆盖面从“后端支持退出”推进到“当前前端的 logout 封装正确调用后端”。回归测试最有价值的来源,往往就是曾经漏掉的真实路径。
两种运行时都要构建和联调
pnpmbuildpnpmstartpnpmbuild:cfpnpmexecwrangler dev--local标准 Next.js Node 运行和 OpenNext Cloudflare Worker 在 Cookie、流式响应、缓存与 Node 兼容层上可能不同。项目分别运行接口联调和页面冒烟,而不是假设 Node 通过就等于 Worker 通过。
故障注入验证恢复,而不只验证错误画面
1. 正常打开搜索页并记录结果 2. 停止独立测试后端 3. 刷新,确认进入错误状态而不是 404/空结果 4. 恢复后端 5. 点击重新加载 6. 确认真实结果与会话状态恢复“页面出现错误提示”只证明失败被展示;最后一步才证明恢复链路有效。
浏览器路径覆盖组合状态
登录 → 刷新恢复 → 首页继续加载 → 收藏 → 会员列表出现 → 退出 → 再访问收藏页 → 跳登录并保留返回路径这条路径同时穿过会话、公开查询、互动失效、会员守卫和安全 redirect。单独测每个组件无法证明它们组合后不互相破坏。
测试数据必须与生产隔离
.local/test.db .local/uploads .local/*.log接口联调会创建账号、草稿、评论和附件,因此只指向独立测试库。环境文件、测试凭证和报告进入忽略目录,不提交到仓库,也不把预览数据当成生产迁移。
当前验证证据
| 检查 | 已记录结果 |
|---|---|
| 类型与规范 | typecheck、Biome 通过 |
| 单元测试 | 会话、URL、Markdown、Cookie、投稿、评论树 |
| 接口联调 | Node 与 Worker 两种环境 |
| 页面冒烟 | 公开/会员、metadata、sitemap、robots、代理 |
| 浏览器 | 登录退出、文章流、搜索恢复、投稿评论、五种宽度 |
| 缓存 | 公开再验证变化、私有 no-store |
旧报告描述旧版本。任何认证、缓存或运行平台改动后,都要按风险重跑相关层级,不能引用历史“全绿”当作新证明。
推荐执行顺序
pnpmgen:apipnpmtypecheckpnpmlintpnpmtestpnpmbuildpnpmbuild:cf之后启动独立后端、Node 生产服务和本地 Worker,运行接口联调、冒烟与浏览器路径。生成、构建和开发服务共享产物目录时应串行,避免交错写入导致假失败。
适用边界
现有证据没有覆盖真实手机硬件、完整跨浏览器、生产压测、新机器零安装和远程 Cloudflare。报告必须保留这些空白,不能因为检查很多就顺带声称全部完成。
质量门禁应按改动风险选择。改一句文案无需跑远程缓存;修改认证内核则不能只看截图。
小结
测试的核心不是数量,而是证据与结论对应。每一层都要说明证明了什么,也要说明不能证明什么。
当故障注入、真实客户端封装、双运行时和组合用户路径都进入门禁以后,“通过”才不再只是一个模糊形容词。
延伸阅读
- Core Web Vitals 与前台性能
如果这篇文章对你有帮助,欢迎订阅我的 CSDN 专栏「成为全栈」:
🔗 专栏地址:https://blog.csdn.net/fungleo/category_13204651.html
📦 本系列配套代码仓库:https://github.com/fengcms/become-a-full-stack-developer