主题
测试策略
这个仓库敢持续重构,靠的是测试跑得快、门禁卡得死。本地一条 npm test 九秒左右跑完 4610 个用例,这是每次重构"改完就知道有没有弄坏"的底气;往上再叠 e2e、用例套件、视觉基线、突变测试,各层各管一段。读完这一页你会知道:每层防线管什么、怎么加测试、以及一次提交要过哪些门禁。
先看全景:测试金字塔
越往下跑得越快、定位越准,所以问题尽量在底层拦住:逻辑写成可直测的纯函数,单测秒级反馈;浏览器里的行为交给 e2e;"看起来对不对"这种最难自动化的部分,交给像素基线和人工验收。这个项目没有搭 Angular testbed 基建,是有意的取舍(ADR-10):不硬模拟 DOM,组件测试以逻辑和模板守卫为主。
单元测试:怎么跑、怎么加
bash
cd editor
npm test # 全量 223 文件 4610 例(2026-10 基线,数字以 CI 为准)
npx vitest run --coverage # 带覆盖率(本地默认不加载 coverage 配置)- 测试放
editor/tests/,目录镜像src/app;mock 管线的测试在editor/mock/。src 下不放测试。 - vitest 跑在 node 环境(不加载 Angular 编译插件,所以快);import 用相对路径,没有
@appalias。 - 用例名写中文行为句;注释只解释 mock 设计、数值推导和回归锚点,不逐行翻译断言。
- 组件和服务测试用
Injector.create({ providers: [...] })最小装配,input signal 用ɵSIGNAL直写;effect 测试套runInInjectionContext;RxJS 时序用useFakeTimers加 TestScheduler marble。 - 覆盖率阈值 95/90/95/95(语句/分支/函数/行),只在带
--coverage运行和 CI 时生效。 - 加新共享纯函数模块必须配单测,先例是
conventions.test.ts。
架构门禁:红即禁合
tests/architecture.test.ts 把开发规范里的分层规则变成机器检查,四条断言:
- shared 到 core、features 的 import 数为零。
- core 到 features 的 import 数为零。
- 模块 import 图无环(DFS 三色标记,值导入口径;import type 编译期擦除不算)。
- 手写
.subscribe(必须同一行配takeUntilDestroyed(双向文本窗口扫描,注释里出现也不行)。
它还会堵各种逃逸写法:双引号 import、动态 import()、裸目录导入都会被归一化后纳入扫描。所以别想着绕,按规则写最省事。
规范与模板守卫
这些测试盯的不是"代码对不对",是"代码和 MapLibre 规范、和模板的事实是否一致":
tests/app/spec-coverage.test.ts拿 style-spec 包里的latest.json当真源逐项对账(槽位、根属性、枚举值、requires)。升级 style-spec 而没补齐中文词表和面板,它就红。tests/app/core/map/map-capabilities.test.ts对账双引擎能力矩阵:代码里调用的每个 native API 必须在矩阵里登记过。tests/app/core/expression-semantics.test.ts用官方createExpression求值对账,防止表达式改写只保结构不保语义。tests/app/core/history/command.test.ts锁跨文档单条撤销的语义。
模板盲区与对冲
这个工具链没有模板类型检查通道,模板里引用不存在的成员,tsc 和 vitest 都抓不到。三类盲区都出过真实事故,各有对冲:
| 盲区 | 事故形态 | 对冲 |
|---|---|---|
| 模板引用不存在的类成员 | 测试全绿,运行时静默空白 | 改公共成员名时同步 grep 模板,或补成员存在性守卫测试 |
| 模板用了 ms-* 组件但 @Component 缺 imports | 静默渲染为空,不报错 | template-imports-guard:模板用法集必须 ⊆ imports 键集 |
| ng-content 被 @if/@for/@switch 包裹 | 投影内容永不渲染(Angular 官方禁令) | 壳组件全模板字节钉 + "单一无条件 ng-content"断言 + e2e |
所以有三类变更必须带对应守卫或 e2e 证据:改组件公共成员名、新子组件模板引用 ms-*/msTip、动壳组件模板。
e2e:三套体系,别混
仓里有三套浏览器测试,定位完全不同:
| 体系 | 规模 | 运行 | 性质 |
|---|---|---|---|
tools/e2e.mjs | 17 套(verify-m65b–m80 + verify-prod) | cd editor && npm run e2e | 回归门禁,大改后必跑 |
e2e-tests/ | 11 套用例套件 | node e2e-tests/run.mjs | 用例文档逐例对应,CI 门禁 |
e2e-audit/ | 15 域 395 例 | node e2e-audit/run.mjs all | 人工审计,只记录不修复,不进任何门禁 |
tools/e2e.mjs(回归门禁):自管 dev server(:4230),先跑十六套里程碑回归(公式、Select data、树、Global、地图工具、Sources、Monaco、Terrain、持久化、mock MVT、Debug、表达式构建器等),最后自动构建最新 dist 跑 verify-prod 生产冒烟。提速开关:node tools/e2e.mjs m67 按名字筛选、E2E_SKIP_BUILD=1 跳过构建(dist 新鲜检测默认开启,输入指纹命中即复用)、并行默认自动开(按 CPU 数分批,verify-prod 恒殿后串行)。
e2e-tests(用例门禁):docs/spec/usecases/ 里十一篇用例文档,每个用例编号(UC-NN)在对应套件里必须有 uc('UC-NN', …) 断言,双向匹配,缺一边就红。CI 里分两步:零浏览器的 traceability 静态门禁(秒级),然后全量 11 套。
e2e-audit(人工审计):395 例全参数矩阵审计(逐样式类型 × 逐参数 × 逐赋值形态),默认 node run.mjs all 跑 01–11 域,12–15 域(截图像素对比、类型矩阵等)要显式指定域号。退出码恒 0——发现问题是审计的产出而不是失败,结论记在 findings.md,修复走正常开发流程。全量约 10–15 分钟,所以不进 CI。
三套共用的纪律:断言"样式世界"的套件必须装载 tools/fixtures/ 下的冻结样式(tools/lib/load-fixture.mjs),不许锚定开发环境的默认装载——默认样式会随网络漂移,历史上酿过整批陈旧断言的事故。
视觉基线
cd tools && node visual-baseline.mjs:11 个 UI 状态全页截图,与 tools/baseline/zh-cn/ 的基线 PNG 做 pixelmatch 对比,超阈值 exit 1。截图用 e2e 验证过的稳定选择器驱动,外域屏蔽、固定视口,保证确定性。
- 有意的视觉变更:
--update重录对应帧,基线 PNG 随 PR 提交,并在提交信息里说明意图。 - nightly 跑在 Linux runner 上,画布区域有跨机 GPU 噪声,阈值放宽到 3%(本地默认 0.4%,可用
VISUAL_TOLERANCE_PCT覆盖)。超阈值先同机复跑确认,再判断是真回归还是噪声。
突变测试与性能基准
npm run mutation(Stryker)回答"测试能不能发现错误":往四个纯函数域(expression.model 与三个 codec)注入变异体,看测试杀不杀得掉。历史分数从 75.67% 补到 94% 量级,扩域后会回落,先定性残余变异体、不盲目刷分。报告在 reports/mutation/(gitignored),incremental 缓存本地留存,本地跑一次要几分钟,不用频繁跑。
npm run bench(tests/app/core/perf/style-bench.bench.ts)是 StyleStore 操作的性能基准,store 读取面做任何 interop 改造前后的对照数据以此为准,不达标不迁移。
一次提交要过的门禁链
CI(.github/workflows/ci.yml)在 PR 和 push main 时跑四个 job,任何一个红了都不算过:
大改后的本地清单
按序过一遍,再提交:
npm run typecheck(随改随跑)。npm test全量绿,例数写进提交信息。- 涉 UI 的变更跑视觉基线对比;有意变更
--update重录随 PR 提交。 - 大改后
npm run e2e全量 17 套(跑前确认 :4230 没有陈旧 dev server,见常见问题)。 - 运行时规范冒烟
node tools/verify-spec26.mjs。 - 行为变更必须带真回归测试;纯重构用现有用例加基线证明零漂移。
测试红了怎么排查,常见原因(端口占用、陈旧 dev server、spec 升级、模板盲区)集中在常见问题。