Cal.diy 测试覆盖率工程规范:新代码 80%+ 覆盖率的 CI 落地与单测实践
【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy
本篇文章以 Cal.diy 仓库的工程规则文档 agents/rules/testing-coverage-requirements.md 为主体,结合仓库真实的 Vitest/Playwright 测试基建,系统讲解这条被标为HIGH 影响的规则:任何 PR 新增或修改的代码都必须达到接近 80% 以上的测试覆盖率,并由 CI 自动强制。读完你不仅能理解"覆盖率门槛为什么存在、卡在什么数值",还能掌握"如何为一段新逻辑写出完整、有语义的单测",以及如何在本仓库中用正确的命令本地验证覆盖率,避免提交被打回。
规则速览:这是一条 CI 强制执行的硬门槛
该规则文件带有结构化的 YAML frontmatter,是仓库内所有工程规则(agents/rules/目录)的统一元数据格式,便于人类工程师与 AI Agent 共同解析:
| 元数据字段 | 值 | 含义 |
|---|---|---|
title | Maintain 80%+ Test Coverage for New Code | 规则标题:为新增代码保持 80%+ 覆盖率 |
impact | HIGH | 影响级别高,与testing-分区的默认级别(MEDIUM-HIGH)相比更高 |
impactDescription | Prevents bugs and enables confident refactoring | 规则的价值:防止 Bug 扩散、支撑大规模重构 |
tags | testing, coverage, quality, ci | 归属维度:测试、覆盖率、质量、CI |
规则正文的核心约束是:每一个 PR 都必须在"它引入或修改的代码"上达到接近 80% 以上的测试覆盖率,且这一要求在 CI 流水线中自动执行。规则给出两个具体判断标准:
- 如果你新增了 50 行代码,这 50 行就必须被测试覆盖;
- 如果你修改了某个已有函数,你的改动本身也必须被测试。
也就是说,门槛不是落在"整个仓库总覆盖率"上,而是落在**增量代码(diff 引入的行)**上。这是一条"防回退 + 促增量"的双向规则:仓库整体质量可以通过既有测试基线托底,而每次提交带来的新逻辑则必须立刻被验证。
三档覆盖率目标的拆解
规则将"覆盖率"进一步拆成三个层次,其优先级与严格程度各不相同:
| 指标 | 目标 | 定位 |
|---|---|---|
| 整体覆盖率(Overall test coverage) | 新增代码 80%+ | PR 级硬性门槛,CI 强制执行 |
| 单元测试覆盖率(Unit test coverage) | 接近 100% | 结合 AI 辅助生成测试后应追求的目标 |
| 全局覆盖率追踪(Global coverage tracking) | 作为关键指标随时间持续改善 | 团队的长期质量方向盘 |
三者的关系可以理解为"台阶":80% 是每一行新代码必须跨越的红线;在单元测试层面,规则明确主张"接近 100%"而不是停在 80%——因为单测成本低、反馈快,覆盖缺口大多可以用 AI 快速补齐;而全局覆盖率则被当作一项持续追踪、只升不降的健康指标,避免团队在个别 PR 上达标却在整体上缓慢退步。
反例与正例:什么才算"带测试的新代码"
反例:无测试的新函数(违规)
规则的"Incorrect"示例非常直白——新增了一个包含复杂逻辑的业务函数,却没有任何对应的测试文件:
// New function with no tests export function calculateAvailability(user: User, date: Date): TimeSlot[] { // Complex logic here... // No corresponding test file }这段代码的问题不在于函数写得不好,而在于没有任何可执行的验证:CI 无法确认它对/错,后续重构无法依赖它,任何改动都只能靠人工肉眼排查。
正例:实现与测试成对交付(合规)
规则的"Correct"示例给出了生产实现与测试文件"结对"的形态:
// calculateAvailability.ts export function calculateAvailability(user: User, date: Date): TimeSlot[] { // Complex logic here... } // calculateAvailability.test.ts describe("calculateAvailability", () => { it("returns empty array for user with no schedule", () => { const user = createMockUser({ schedules: [] }); expect(calculateAvailability(user, new Date())).toEqual([]); }); it("excludes busy times from available slots", () => { const user = createMockUser({ schedules: [mockSchedule], busyTimes: [mockBusyTime], }); const slots = calculateAvailability(user, new Date()); expect(slots).not.toContainEqual(expect.objectContaining({ start: mockBusyTime.start, })); }); it("handles timezone conversions correctly", () => { // Test timezone edge cases }); });细看这三个用例,其实对应着"可用性时段计算"这类领域函数(在 Cal.diy 中与 packages/features/slots 等模块的职责同源)必须覆盖的三类行为:
- 空输入 / 边界基准:用户没有任何日程时返回空数组,锁定函数的"零输入"契约;
- 核心业务分支:日程中的 busy times 必须从可用时段中剔除,断言用
not.toContainEqual(expect.objectContaining({ start: mockBusyTime.start }))只关心"冲突时段起点是否混入结果",而不与具体 Slot 对象强耦合——这正是对TimeSlot[]这类结构进行部分匹配断言的典型写法; - 跨时区转换:时区边界处理(夏令时、跨日换算等)往往单独开用例钉死,因为这类 Bug 只在特定
TZ环境下方可复现。
这段示例还透露了仓库测试的通用骨架:用describe/it组织用例、用createMockUser(...)这类工厂函数构造最小可用的输入、用expect(...).toEqual(...)做结构性断言。仓库中真实的单测也完全遵循这一形态,例如 packages/lib/array.test.ts 对uniqueBy的测试同样覆盖了"单键去重、多键去重、空数组、单元素数组"四条路径,与示例中"空输入 + 主分支 + 边界"的枚举思路一致。
回应"覆盖率不能说明一切":为什么仍要定 80%+
规则原文专门预留了一段对常见质疑的正面回应:
是的,我们知道覆盖率不能保证测试是完美的;我们知道可以写出每行都命中、却什么也不验证的"无意义测试";我们知道覆盖率只是众多指标之一。但瞄准一个高百分比,总好过对自己身在何处毫无概念。
这段话其实是给团队立了一个"度量哲学":覆盖率不是质量本身,而是可观测的方向标。没有方向标,代码质量的讨论就退化为"我觉得没问题"的主观判断;有了 80%+ 的红线,至少保证了每次迭代都在可量化、可对比的轨道上前进,剩下的质量深化(断言的语义强度、Mock 的真实度)再由 agents/rules/testing-mocking.md 等其他规则去约束。
用 AI 补足单测:把"接近 100%"从口号变成现实
规则的落点非常明确:AI 可以快速且智能地构建完整的测试套件,纯手工测试正在越来越成为过去式。这条主张与仓库的整体定位是一致且自洽的——agents/目录下的整套规则体系本身就被设计为"机器可读",供 AI 编码 Agent 解析执行(见 agents/rules/README.md)。
实操含义是:当一个 PR 中 80% 红线已满足、但想把单测逼近 100% 时,正确姿势是把代码片段与领域规则交给 AI 生成用例骨架,再人工补充断言语义、修正 Mock。需要特别注意与之配套的 Mock 纪律(详见后文"配套纪律"小节):AI 生成的测试如果不遵守仓库的 Mock 接口约定,反而会引入类型兼容问题。
Cal.diy 仓库里的测试基建:规则背后的真实支撑
覆盖率规则要落地,离不开仓库测试体系的支撑。Cal.diy 的单测全部运行在 Vitest 之上,关键配置可以从仓库中直接核对:
- 测试入口命令:根 package.json 的
test脚本为TZ=UTC vitest run,默认强制 UTC 时区;另有tdd(vitest watch)用于开发循环、type-check:ci用于类型闸门。 - 覆盖率提供器:vitest.config.mts 的
test.coverage显式配置了provider: "v8",配合 devDependencies 中的@vitest/coverage-v8。本地需要量化某个文件/某个 diff 的覆盖率时,可直接追加--coverage参数运行:
# 本地跑覆盖率(UTC 时区 + v8 提供器,与 CI 同源) TZ=UTC vitest run --coverage- 环境与别名:同文件还配置了 jsdom 环境、
forks线程池、@calcom/*源码内联解析,以及指向 packages/testing/src/setupVitest.ts 的 setup 文件——后者负责在 React 组件渲染前预置window.matchMedia等 jsdom 缺失的浏览器 API。这意味着仓库内大量"纯逻辑函数"(如 packages/lib/array.ts、packages/lib/slugify.ts、packages/lib/crypto.ts等,各自都有同名.test.ts成对存在)与"UI/交互逻辑"都能在一个测试体系下被覆盖。 - 特殊测试模式的编排:vitest.workspace.ts 通过
VITEST_MODE环境变量切分出integration、timezone(如VITEST_MODE=timezone时要求必须显式提供TZ,否则直接抛错)、packaged-embed等 workspace,并把命名为*.timezone.test.ts、*.integration-test.ts的文件归入对应模式。这直接支撑了规则中"处理时区边界用例"那类测试的可复现性。
结合 agents/rules/testing-timezone.md 可以看到仓库在这方面的共识:时区 Bug 极难复现,因此测试环境必须时刻保持一致——本地开发机是Asia/Shanghai、CI 是UTC,同一个日期断言就可能一个绿一个红。仓库的解法就是命令层面统一TZ=UTC(已在package.json的test脚本中固化),把不确定性从源头掐掉。
让覆盖率转化为真实质量的四条配套纪律
80%+ 覆盖率解决的是"有没有测",而下面几条同目录规则解决的是"测得好不好、能不能稳定通过":
- 先类型、后测试:agents/rules/ci-type-check-first.md 规定修复顺序是
yarn type-check:ci --force先行——类型错误往往是测试失败的根因,先消类型错误能切断级联失败;若报缺失枚举/类型(如CreationSource.WEBAPP),先跑yarn prisma generate从 Prisma schema 重新生成类型。 - 逐文件增量修复:agents/rules/testing-incremental.md 建议一次只解决一个文件,把每个文件的用例跑绿再进入下一个,避免被多文件失败压垮。
- Mock 遵守接口而非照抄结构:agents/rules/testing-mocking.md 指出:Mock 日历服务时应实现统一的
Calendar接口(因为所有日历服务都实现该接口并被存入 map),而不是按某个具体服务类型逐个加属性;对 app-store 资源则优先实现简洁接口而非堆叠mockDeep的深层结构——不良 Mock 是 flaky 测试与误报的源头。 - E2E 与单测分层、本地先行:agents/rules/testing-playwright.md 规定端到端测试统一走
PLAYWRIGHT_HEADLESS=1 yarn e2e [test-file.e2e.ts](自带时区、虚拟显示与仓库 e2e runner),禁止直接调用yarn playwright test;且强调 E2E 必须先本地跑通再推送,CI 仅在 PR 打上ready-for-e2e标签时执行。
单测的 80%+ 保证"每个函数的行为被锁定",E2E 保证"跨模块链路集成正确",二者结合才构成规则开篇impactDescription所说的"防止 Bug + 支撑自信重构"。
提交前的覆盖率自查清单
综合规则与仓库实践,一个合规 PR 在推送前应通过以下检查:
- 新增/修改的代码行都有对应测试文件,且能单独运行(
TZ=UTC vitest run path/to/file.test.ts); - 用例覆盖空输入、核心业务分支、时区/边界等关键路径,而非只覆盖 happy path;
- 本地覆盖率(
TZ=UTC vitest run --coverage)达到 80%+,单测尽量逼近 100%; yarn type-check:ci --force无新增类型错误,缺失枚举已用yarn prisma generate修复;- 测试在
TZ=UTC下运行,Mock 遵守目标接口而非复制深层结构; - E2E 改动已用
PLAYWRIGHT_HEADLESS=1 yarn e2e在本地验证通过。
把这六条落实到位,80%+ 的覆盖率红线就不再是 CI 上的一次红色告警,而会成为团队"基础设施几乎从不失败"这一工程哲学里最基础的一环。
【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考