项目:nestjs-admin | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
本文盘点"测试都有什么、门禁卡在哪、缺口在哪"。部署运行命令速查见《06-部署运行》§5,本章深入每个测试文件的内容、代表性用例的考察意图,以及一套务实的补测路线。
| 层级 | 文件 | 用例数 | 覆盖对象 | 运行条件 |
|---|---|---|---|---|
| 网关单测 | gateway/src/consul/consul.service.spec.ts | 6 | 服务发现:健康实例选取、指数退避、双 Consul 竞速、恢复广播、5s 重试不抛未处理拒绝 | mock ConsulHttpClient 与 ClientProxyFactory,无需真实 Consul |
| 网关单测 | gateway/src/auth/auth.service.spec.ts | 6 | signin 双 token 签发/密码错 403/空对象判定、signup 建号/已存在 403/gRPC 失败触发客户端重建 | mock argon2 与 JwtService |
| 网关单测 | gateway/src/auth/authorization.service.spec.ts | 8 | CASL 规则编译、inverted 覆盖、双源合并(perm 码)、按用户缓存、无角色短路、gRPC 故障 fail-closed | mock 两个 gRPC 客户端槽位 |
| 网关单测 | gateway/src/auth/policies/policies.guard.spec.ts | 4 | 无元数据放行、无用户身份拒绝、能力满足放行、能力不足 403 | mock Reflector 与 AuthorizationService |
| 网关单测 | gateway/src/common/interceptors/serialize.interceptor.spec.ts | 2 | @Exclude 剔除 password、白名单模式剥离未知字段 | 纯 rxjs |
| 网关 e2e | gateway/test/app.e2e-spec.ts | 4 | GET /api 200、未带 token 401、signup 参数非法 400、缺字段 400 | 需本机 Consul 8500/8501 在线;不依赖 user 服务与数据库 |
| system 单测 | system/src/app.controller.spec.ts | 7 | 登录/操作日志的分页 skip/take、status 1/0/-1 筛选语义、模糊+时间范围 where 构建、返回形状 | mock Prisma 两个 model |
| user 服务 | 无任何 *.spec.ts | 0 | — | pnpm test 报 jest "0 matches" 属正常(HANDOFF 陷阱 21) |
| health/template | 各自 test/app.e2e-spec.ts | 骨架 | 模板生成的默认 e2e,未写业务断言 | — |
| 全链路冒烟 | nestjs-microservice-app/smoke_fullchain.js | 4 步 | signin → my-menus(顶级菜单数)→ dashboard 七项 → 验证码接口已下线(404),输出 ALL GREEN | 需全栈已启动;受 signin 10/min 限流约束 |
| 前端 | 无 vitest/jest 配置 | 0 | 质量靠 typecheck + lint + 人工巡检 | — |
计数口径说明:网关单测合计 26 个 it(jest 实跑复核 Tests: 26 passed, 26 total,与 HANDOFF 多处记录的 26/26 全绿一致),五份 spec 的分布是"consul 6 + auth 6 + authorization 8 + policies 4 + serialize 2"。测试环境的两个硬约束必须复述:jest 30 + NestJS 12 的 ESM-only 包要求 NODE_OPTIONS=--experimental-vm-modules(已写进 test 脚本,cross-env 注入);Node 需 ≥24.9(建议 nvm v24.21.0 并 PATH 前置,zsh 非交互 shell 里 node 可能是 nvm 懒加载包装函数,脚本内用绝对路径)。
consul.service.spec.ts)这组测试的价值在于它锁定了三个"手工极难验证、回归极伤"的时序行为。其一,retryGetServiceInfo 的容灾竞速:consul1 空列表、consul2 命中时,结果必须来自 consul2——测试用 jest.useFakeTimers() + advanceTimersByTimeAsync 驱动 rxjs 的 timer,这是测 retry/退避逻辑的标准姿势。其二,恢复广播:两个 Consul 各持一条 nestjs-service 客户端条目,订阅挂在"落选"条目(consul2)上,consul1 的 updateGrpcClient 恢复后断言 consul2 上的订阅者也收到了非空客户端——这正是 updateGrpcClient 里 filter(c => c.serviceName === serviceName) 全量广播语义的回归锚点,改回"只更新自己"这条测试立刻红。其三,错误卫生:发现失败时构造器内的 5 秒重试链不能产生未处理 Promise 拒绝(Node 会因此崩进程),测试推进 40 秒虚拟时间断言不抛。mock 组织上有个精巧设计:ConsulHttpClient 构造函数同步消费"预置健康结果队列"(mockHealthQueue),使每个 ConsulService 实例在构造期就拿到各自的发现结果——这是被"构造器内 initClients 立即发起首次发现"的现实逼出来的 mock 形态。
auth.service.spec.ts)用户不存在时抛出 403 而不是对空对象做校验 这条用例锁定了 gRPC 边界的一个隐形契约:user 服务 FindOne 未命中返回 {} 而非 null,网关必须以 user.id 判存在性——若误写成 if (!user),空对象为真值,后续 argon2.verify(undefined, ...) 会产生难以理解的错误。同文件的 gRPC 调用失败时触发客户端重建回调 锁定自愈机制:构造 details='No connection established'、message='connect ECONNREFUSED' 的错误(与 GrpcExceptionInterceptor.checkIfConnectionIssue 的判定字段精确对齐),断言 updateGrpcClient 被以正确服务名调用。注释还提醒了一个测试写法陷阱:必须持有 promisify 包装前的原始 jest.fn 引用,否则断言打在包装函数上永远不命中。
system/src/app.controller.spec.ts)七条用例完整钉死了日志查询的边界约定:无分页参数时保持历史行为(skip 0/take 100、orderBy id desc);page=2&pageSize=5 换算 skip=5,take=5 且返回过滤后总数;status=1 映射"成功"(登录日志 success:true;操作日志 statusCode 2xx)、status=0 映射"非成功"(操作日志走 OR 分支:<200 或 >=300)、-1 不加筛选(proto3 默认值 0 与"未传"区分是 M9 修过的坑);username 模糊(contains)与时间范围(gte/lte)组合进同一 where。这组测试的存在理由写在 HANDOFF 里:登录日志页曾因同类逻辑缺陷"空表运行了数个里程碑",此后这类条件组合被认定为高回归风险区。
门禁分四道,按"提交前必跑 → CI 自动 → 发布前人工"分级:
三处细节值得展开。CI 与本地的差异:.github/workflows/ci.yml 起了 Consul 与 Postgres 服务容器,因此 e2e 能真实装配 AppModule(发现链路会去查 Consul),而本地跑 e2e 前提是 dev-start.sh 的两个 consul dev agent 在线;CI 只对 user 库跑了 migrate deploy(system 库未跑,因 e2e 不触达 system_db)。前端门禁的严格度:vue-admin/package.json 的 lint 是 eslint --max-warnings 0(一个警告都不过)+ prettier 全量检查,typecheck 是 vue-tsc --noEmit --skipLibCheck,build 用 8GB heap + rolldown 分包——HANDOFF 各里程碑的验收标准都是"后端 build+单测、前端 typecheck+build"成对出现。人工验收基线:全链路冒烟脚本的四步断言(登录/菜单/仪表盘/验证码下线)加浏览器人工过一遍核心页面,是每次发版的固定动作;历史上一轮 M7 冒烟 21 项、M8 冒烟 25 项的一次性脚本(/tmp/smoke_m7.py、smoke_a1a7.py)已被系统清理,仅存 HANDOFF 记录,其断言思路(按"目标 id 消失"而非数量归零断言日志删除)值得在重建冒烟套件时继承。
按"回归概率 × 爆炸半径"排序,当前体系有七个明确缺口:
CreateUser 的事务/角色校验/P2002 映射、DeleteDept 的双前置校验都是纯逻辑密集区,参照 system/src/app.controller.spec.ts 的 mock-Prisma 模式补齐成本很低,建议最先补 CreateUser/UpdateUserRoles/DeleteDept 三条。request-logging.middleware.ts 的路径分流规则(signin/signup→登录日志、写方法→操作日志、module 取 URL 前两段)与 all-exceptions.filter.ts 的 gRPC→HTTP 映射表都是"改一行影响全站观测"的集中点,且都是纯函数式逻辑,非常适合单测。GrpcExceptionInterceptor 的连接失败判定(No connection established + ECONNREFUSED 双条件)同理。utils/http/index.ts 的刷新队列写 vitest 单测(时序复杂、401 全局登出的兜底行为值得锁定),再给 api/routes.ts 的 toRoute/buildTree 写纯函数测试(出错影响全站导航但输入输出确定),页面级 E2E(Playwright)最后考虑——pure-admin 演示页多、选择器不稳定,投入产出比低。AuthorizationService 的缓存失效行为补一条。三条从现有体系提炼出的工作纪律。第一,改三类代码必须跑对应测试:动 gateway/src/consul/** 跑 consul.service.spec,动日志查询跑 system 的 app.controller.spec,动 auth 链路跑 auth/authorization/policies 三份 spec——它们锁定的都是"曾经真实坏过"的行为,绿了再提交。第二,新增接口的最小测试义务是给网关控制器补一条权限码守卫断言(policies.guard.spec 的 makeContext 模式可直接复用),给业务服务补一条成功路径 + 一条约束冲突路径(P2002/前置条件),这两类正是线上事故的高发区。第三,把"冒烟 ALL GREEN + 人工过页"当作不可省略的发布门禁,尤其在改过 proto、seed、迁移或会话层之后——现有单测的覆盖重心在网关与查询构建,业务读路径的最终保证仍来自全栈实跑,这在教学/演示规模下是有意为之的分级策略,不要误以为"测试全绿 = 可以直接发"。