ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

cal.diy 测试 Mock 实现模式指南:以 Calendar 接口为核心,构建稳定、类型安全的日历服务与 App-Store 测试

cal.diy 测试 Mock 实现模式指南:以 Calendar 接口为核心,构建稳定、类型安全的日历服务与 App-Store 测试 cal.diy 测试 Mock 实现模式指南以 Calendar 接口为核心构建稳定、类型安全的日历服务与 App-Store 测试【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy本篇技术指南基于 cal.diy开源调度基础设施仓库的工程规范文档 agents/rules/testing-mocking.md系统讲解测试文件中日历服务与 App-Store 集成资源的 Mock 实现模式为什么 Mock 必须实现统一的Calendar接口、为什么应当避免mockDeep深拷贝式 Mock、以及在类型冲突难以解决时如何优雅退化为手写 Fake 实现。读完本文你将掌握一套可直接应用于本项目测试代码的可维护 Mock 范式从根本上降低由 Mock 引起的测试脆弱性与假阳性风险。背景差的 Mock 是脆弱测试与假阳性的根源该规则文档在 Frontmatter 中明确声明了其影响等级与原因impact: MEDIUMimpactDescription: Poor mocks cause flaky tests and false positives糟糕的 Mock 会导致测试不稳定与假阳性结合仓库的测试实践可以理解这一论断的深层原因cal.diy 的测试大量使用 Vitest 的vi.mock来替换第三方日历服务、App-Store 应用模块等重型依赖。如果 Mock 只是从具体服务类型上零散复制几个属性那么一旦真实服务的类结构变化新增方法、改变返回类型、调整私有字段Mock 就会与类型系统脱节出现两类典型症状类型兼容性问题Mock 对象与消费方期望的接口类型不符编译阶段tsc报错导致测试代码无法推进运行时假阳性Mock 只覆盖了部分方法被测代码走到未 Mock 的分支时静默拿到错误结果或抛异常测试看起来绿了实际却根本没有验证目标行为。因此该规则给出的核心方法论可以概括为一句话Mock 的形态应追随被测代码所依赖的契约接口而非追随某个具体实现类的内部形状。认识被测主体日历服务与Calendar接口在理解 Mock 规则之前需要先了解生产代码中日历服务的组织方式因为 Mock 规则正是为贴合这一结构而设计的。所有日历服务统一实现Calendar接口Calendar接口定义在 packages/types/Calendar.d.tsCalendar.d.ts中interface Calendar第 294 行起。它规定了日历服务必须提供的完整方法集合方法签名要点语义getCredentialId?(): number返回凭据 ID可选createEvent(event: CalendarServiceEvent, credentialId: number, externalCalendarId?: string) PromiseNewCalendarEventType创建日程事件updateEvent(uid: string, event: CalendarServiceEvent, externalCalendarId?: string \| null) PromiseNewCalendarEventType \| NewCalendarEventType[]更新日程事件deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string \| null) Promiseunknown删除日程事件getAvailability(params: GetAvailabilityParams) PromiseEventBusyDate[]获取忙闲时间可用性供选时段使用getAvailabilityWithTimeZones?(params: GetAvailabilityParams) PromiseEventBusyDate[]带时区解析的可用性可选目前仅 Google 日历fetchAvailabilityAndSetCache?(selectedCalendars: IntegrationCalendar[]) Promiseunknown拉取可用性并写缓存可选listCalendars(event?: CalendarEvent) PromiseIntegrationCalendar[]列出日历testDelegationCredentialSetup?(): Promisevoid校验委托凭据配置可选在具体实现层所有日历服务都严格实现该接口例如packages/app-store/feishucalendar/lib/CalendarService.ts 第 35 行class FeishuCalendarService implements Calendar随后通过构造函数接收CredentialPayload完成鉴权与初始化packages/lib/CalendarService.ts 第 388 行抽象基类BaseCalendarService implements Calendar被 CalDAV、Exchange 等多项服务复用。日历服务以映射表 动态导入的方式注册具体服务不会在调用方被直接实例化。生成的注册表 packages/app-store/calendar.services.generated.ts 定义了一个CalendarServiceMap把日历类型字符串映射到动态 import返回的模块 Promiseexport const CalendarServiceMap process.env.NEXT_PUBLIC_IS_E2E 1 ? {} : { applecalendar: import(./applecalendar/lib/CalendarService), feishucalendar: import(./feishucalendar/lib/CalendarService), googlecalendar: import(./googlecalendar/lib/CalendarService), // ... 其余日历类型 };而运行时工厂 packages/app-store/_utils/getCalendar.ts 的getCalendar()会从该映射表中按凭据的type字段取出对应的导入函数、执行calendarApp.default得到构造器并创建实例最终统一返回类型为Calendar | null的服务对象。由此可以理解规则文档第一段Since all calendar services implement theCalendarinterface and are stored in a map所有日历服务都实现了Calendar接口并被存储在映射表中所描述的架构事实消费方只依赖接口与映射表从不直接依赖某个具体日历类。Mock 若破坏这一约定就等于把映射表机制替换成了一套与生产形态无关的假结构。Calendar Service Mocks实现整个Calendar接口而非逐属性复制规则文档的核心指令非常明确When mocking calendar services in Cal.diy test files, implement theCalendarinterface rather than adding individual properties from each specific calendar service type (likeFeishuCalendarService).即测试文件中 Mock 日历服务时应当实现完整的Calendar接口而不是从某个具体日历服务类型如FeishuCalendarService上逐个复制属性。推荐做法Mock 工厂返回满足接口形状的完整对象仓库中 packages/features/calendars/lib/getCalendarsEvents.test.ts 是该模式的教科书式范例。它通过vi.mock(calcom/app-store/calendar.services.generated)在测试层替换掉生成的映射表并为每个日历 key 提供 Promise 化的 Mock 模块const mockGoogleGetAvailability vi.fn().mockResolvedValue([]); const mockGoogleGetAvailabilityWithTimeZones vi.fn().mockResolvedValue([]); vi.mock(calcom/app-store/calendar.services.generated, () { return { CalendarServiceMap: { googlecalendar: Promise.resolve({ default: (credential: { id: number }) ({ getCredentialId: () credential.id, createEvent: vi.fn().mockResolvedValue({}), updateEvent: vi.fn().mockResolvedValue({}), deleteEvent: vi.fn().mockResolvedValue({}), getAvailability: mockGoogleGetAvailability, getAvailabilityWithTimeZones: mockGoogleGetAvailabilityWithTimeZones, listCalendars: vi.fn().mockResolvedValue([]), }), }), office365calendar: Promise.resolve({ default: (credential: { id: number }) ({ // 同样的完整方法集合…… }), }), }, }; });这段 Mock 的形态与生产结构严格对应模块边界一致Mock 的是CalendarServiceMap所在的生成模块与 getCalendar.ts 的 import 源一致导入结构一致每个 key 的值是Promise.resolve({ default: factory })与动态import()产生的模块形状相同返回对象与接口一致工厂返回的对象拥有getCredentialId / createEvent / updateEvent / deleteEvent / getAvailability / getAvailabilityWithTimeZones / listCalendars恰好对应Calendar接口的全部非可选成员。正因返回对象完整实现了接口无论后续是直接调用getAvailability、createEvent还是被getCalendar工厂消费被测代码都能以类型安全的方式工作而getAvailability等关键方法被提取为可复用的vi.fn()变量如mockGoogleGetAvailability便于在每个用例中单独改写返回值——这正是既类型安全又可控的 Mock 设计。反面做法从具体类型零散拼属性为什么危险反例是文档点名批评的做法Mock 时照抄某个具体服务如FeishuCalendarService中用到的一两个属性而不是面向接口补齐契约。假设代码逻辑依赖feishucalendar的某一个私有辅助字段测试 Mock 就照抄该字段、跳过其余方法这会带来三个问题类型兼容性崩塌消费方例如被 Mock 的对象会流入一个接收Calendar参数的函数期望整个接口Mock 却缺少updateEvent、deleteEvent等方法tsc直接报错最终只能靠as断言强行捂住类型错误让测试失去类型保障与映射表机制脱节生产代码通过CalendarServiceMap动态加载服务Mock 若绕开该结构、单独实例化具体类就无法复现真实的加载路径测试覆盖到的是假接线而不是真实调用链重构脆弱性具体服务类的内部属性属于实现细节、最易变动Mock 依赖实现细节越多服务端重构时测试越先崩。E2E/集成环境下的特例说明值得注意映射表在NEXT_PUBLIC_IS_E2E 1时本身会被生成为空对象{}见 calendar.services.generated.ts说明在 Playwright E2E 场景中日历服务会被整体禁用。这也是为什么 Mock 方案要统一收敛在接口层——不同测试层级可以各取所需但契约保持一致。App-Store Integration Mocks用简单、直接的接口 Mock 取代mockDeep规则文档的第二条指令针对 App-Store 集成测试When mocking app-store resources in Cal.diy tests, prefer implementing simpler mock designs that directly implement the required interfaces rather than trying to match complex deep mock structures created withmockDeep.即测试中 Mock App-Store 资源时优先采用直接实现所需接口的简单设计而不是费力去复刻mockDeep生成的深层 Mock 结构。mockDeep的适用场景与问题根源mockDeep来自vitest-mock-extended能递归地把一个复杂对象的每一层都变成vi.fn()。仓库中确实存在合理使用它的场景例如 packages/features/calendars/lib/mocks/CalendarManager.tsimport { mockReset, mockDeep } from vitest-mock-extended; import { CalendarManager } from ../CalendarManager; const CalendarManagerMock mockDeeptypeof CalendarManager();当被测目标本身是一个大而全的管理器类如聚合多个日历操作的CalendarManager时mockDeep可以快速产出完整 Mock。但它的代价是Mock 的结构深度与类型体操显著复杂。当模块之间存在循环依赖、联合类型、泛型或品牌类型时mockDeepT的推断极易产生类型兼容性冲突——这正是规则文档所说 complex deep mock structures 与 type compatibility issues 的由来。而对大多数 App-Store 集成测试来说被测代码真正调用的往往只是某个应用卡片组件、工具函数或服务的少量公开方法。为了一两个方法引入整棵深拷贝 Mock 树属于不必要的复杂度既难维护也容易在服务接口微调时牵一发而动全身。推荐做法手写返回所需接口的轻量 Fake更优的方案是像getCalendarsEvents.test.ts那样直接用普通对象 vi.fn()组装出恰好满足被测代码所需接口的轻量 Fake。这类设计的特点只声明被测代码实际调用的方法其余方法可省略前提是消费方类型允许方法返回值显式可控测试意图一目了然不依赖vitest-mock-extended的深层推断绕开类型兼容性雷区易于重构改一个方法签名只影响一处。大规模自动化 Mock 场景Proxy 化映射表当测试需要同时覆盖大量日历/视频服务、又不想为每个服务手写 Mock 时仓库给出了一个创造性的简单方案——见测试基建 packages/testing/src/lib/bookingScenario/bookingScenario.ts 第 52-70 行。它以Proxy包装CalendarServiceMap任何 key 被访问时都自动返回一个标准化的 Mock 构造器// 共享的 Mock 日历构造器集合vi.mock 工厂与 mockCalendar 都可访问 const calendarServiceConstructorMocks: Recordstring, ReturnTypetypeof vi.fn {}; function getOrCreateCalendarServiceMock(key: string): ReturnTypetypeof vi.fn { if (!calendarServiceConstructorMocks[key]) { calendarServiceConstructorMocks[key] vi.fn(); } return calendarServiceConstructorMocks[key]; } vi.mock(calcom/app-store/calendar.services.generated, () ({ CalendarServiceMap: new Proxy({} as Recordstring, Promise{ default: ReturnTypetypeof vi.fn }, { get(_target, prop: string) { if (typeof prop symbol) return undefined; return Promise.resolve({ default: getOrCreateCalendarServiceMock(prop) }); }, }), }));这段代码同时示范了规则文档的三条通用指导按需惰性 MockProxy的get钩子保证用到哪个服务才 Mock 哪个服务测试只会为实际触碰的日历类型创建构造器避免真实模块如 feishu/lark被 import 进测试进程——注释明确指出这能阻止工作进程关闭阶段触发额外的异步 fetch 调用模块形状与接口保持简单每个 key 仍返回Promise.resolve({ default: mock })这一统一结构与生产动态 import 的模块形状、与CalendarServiceMap的消费方式完全一致共享可变状态被显式管理构造器 Mock 集中在模块级 Map 中供vi.mock工厂与测试辅助函数共同读写。General Guidance三条可落地的工程准则规则文档的第三部分给出三条操作性极强的通用建议它们在仓库中都有对应实践逐一展开如下。1. 复杂 Mock 遇到类型问题时退化为简单 Fake 实现For complex mocks that cause type compatibility issues with deep mocks, consider using simpler fake implementations.简单 Fake不是妥协而是一种主动的设计选择当mockDeep等工具与模块类型产生不可调和的冲突时一个手写的、只实现被测路径所需接口的 Fake 对象往往比在类型体操里挣扎更快、更稳。判断标准是被测代码真正依赖的最小接口面——面向最小接口写 Fake既能通过类型检查也天然聚焦测试意图。2. 必要时允许修改其他 Mock 文件以支持当前实现When needed, you can modify other mock files to support your implementation.Mock 之间常有共享依赖例如全局prismaMock、通用模块 Mock。当一个用例的 Mock 需要其他 Mock 的配合才能成立时规则明确允许修改配套 Mock 文件。仓库中同样能看到这种配套 Mock 协同的实践例如packages/testing/src/lib/bookingScenario/bookingScenario.ts 同时vi.mock了calendar.services.generated、video.adapters.generated与calcom/lib/crypto用importOriginal保留真实实现、只包装加解密函数多个 Mock 协同构造完整场景getCalendarsEvents.test.ts开头先vi.mock(calcom/lib/crypto, () ({ symmetricDecrypt: vi.fn() }))再借助vi.mocked(symmetricDecrypt)在用例内灵活编排返回值。这一准则是重构自由的体现当标准 Mock 成为实现目标的阻碍时调整共享 Mock 的形态以服务整体测试设计是正当行为。3. 标准 Mock 引发持续性类型错误时鼓励创造性重构Creative solutions and refactoring to better designs are encouraged when standard mocking causes persistent type errors.最深层的一条原则如果 Mock 代码反复出现类型错误往往不是 Mock 写法的问题而是被测代码的抽象边界本身值得重新审视。此时鼓励向上游重构——调整接口划分、收敛依赖、让生产代码消费更小的接口面——从而让测试自然变得简单。这条建议把 Mock 问题从测试层的修补提升到了设计层的改进避免用层层as断言去掩盖架构债。落地自检清单在 cal.diy 仓库中新增或修改测试 Mock 时可对照以下清单进行 Review对应 CI 中 TypeScript 检查与测试稳定性的常见失败点Mock 日历服务时返回对象是否完整实现Calendar接口而非从FeishuCalendarService等具体类复制属性Mock App-Store 资源时是否采用了直接实现所需接口的简单对象而非盲目堆叠mockDeepMock 的模块路径与导出形状是否与生产加载链一致如CalendarServiceMap的Promise{ default }结构是否避免用as断言强行掩盖 Mock 与接口之间的类型缺口当类型冲突持续存在时是否考虑过简化 Fake、调整配套 Mock、甚至重构被测抽象而不是继续在原有方案上加补丁遵循以上模式Mock 才能真正成为稳定的测试地基而非脆弱测试与假阳性的来源——这正是 agents/rules/testing-mocking.md 希望每一位仓库贡献者内化的工程共识。更完整的 Mock 设计规范可进一步参考 agents/rules/testing-incremental.md 与 agents/rules/testing-mocking.md 同目录下的其他测试规则以及测试基建入口 packages/testing/src/lib/bookingScenario/bookingScenario.ts。【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表