关于 writing-tests
面向 Remotion 代码库的技能,指导选择测试边界,并编写验证用户可见契约的测试。它涵盖 UI、CLI、渲染等工作流的测试选择,强调完整结果而非实现细节。
主要功能
- 01选择稳定的测试边界
- 02优先验证可观察结果
- 03指导集成与端到端覆盖
- 04界定单元测试的适用场景
- 05建议验证回归测试
适用场景
- 01规划功能的测试覆盖
- 02端到端测试 CLI 工作流
- 03验证渲染或持久化结果
- 04评估缺陷修复的回归测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ remotion-dev/ remotion --skill writing-tests安装后在 Claude Code 中输入/writing-tests确认已加载。
详细介绍
编写测试
当用户明确要求新增测试,或功能开发、Bug 修复、重构或 PR 准备需要更新现有测试、fixture 或快照时,应用此技能。除非用户明确要求,否则不要添加新测试,包括在现有文件中添加新的测试用例。要求审查测试并不意味着可以修改测试。
测试的目的是让用户确信,Studio 用户、CLI 用户、包使用者或渲染器使用者所依赖的工作流或契约能够正常运行。
在能提供有效信心的稳定边界中,选择最广的边界进行测试。优先选择一个完整的集成工作流;只有逻辑确实复杂时才使用针对性的单元测试;对于关键的跨进程工作流,则使用 E2E 测试。
测试数量、断言数量、文件数量和覆盖率百分比都不是质量指标。一次渲染帧、最终产物或持久化源文件断言,可能比许多范围狭窄的单元断言更有说服力。
如果功能断开后测试仍能通过,那么它就不适合作为主要覆盖。对于面向用户或跨层功能,不能只依靠私有辅助函数的单元测试来覆盖。
编写测试前先选择边界
先回答以下问题:
- 用户或公共 API 使用者会注意到什么故障?
- 哪个最广的稳定边界能以合理成本复现该故障?
- 是否有现成的集成或 E2E 工作流可以扩展?
- 如果功能入口不再调用受测辅助函数,拟议的测试会失败吗?
- 哪些边缘情况(如有)仍值得用针对性的单元测试覆盖?
测试类型由边界决定,而非目录或测试运行器。Bun 测试可以是集成测试,而 Playwright 测试也可能仍与实现细节紧密耦合。
使用以下主要边界:
| 变更 | 首选的主要覆盖范围 |
|---|---|
| Studio 交互 | 执行 Playwright 操作,然后检查可见结果、源文件、持久化结果、URL 或渲染结果 |
| Studio Server 或 codemod | 通过路由或处理程序传入逼真的源文件 fixture,然后检查生成的源文件或事件 |
| Core、Player 或 Media | 挂载或渲染真实组件,并使用真实的相邻模块和有代表性的媒体 |
| Web 渲染器或视觉效果 | 浏览器渲染、图像比较、像素探测或生成的帧 |
| CLI | 执行命令并断言退出代码、输出和文件系统影响 |
| 渲染器、SSR、模板或打包 | 通过 packages/it-tests 使用公共 API,或使用逼真的临时项目 |
| Lambda 或云端编排 | 执行完整编排,只模拟或伪造远程提供方 |
| 纯数学、解析器、序列化器、协议转换或状态机 | 使用有针对性的单元测试,最好采用表格驱动方式 |
不要强行让每项变更都通过 Playwright 测试。路由级或文件系统集成通常是最稳定的边界。只有对于涉及浏览器、服务器、文件系统、HMR 或进程边界的重要风险,才使用完整的 E2E 测试。
断言契约与结果
优先断言用户或集成方能够观察到的结果:
- 渲染帧、像素、媒体时间戳、图像输出和视觉回归
- 生成或持久化的源代码和文件
- 公共返回值、事件、错误和协议消息
- CLI 退出状态、输出、提示词和文件系统变更
- Studio 文本、可访问名称、焦点、启用状态、导航和持久化
- 对外部边界的请求,包括作为契约一部分的方法、路径和负载
优先断言能够证明执行路径已运行的正向最终结果。单独断言 does not throw、not visible、not present 或“错误不存在”说服力不足。应先断言预期的重排、文件变更、请求、渲染输出、持久化状态或成功结果;只有在负向断言能够增加额外信心时才添加它。
当生成的代码是用户可见的产物时,断言源代码是合理的。断言有意义的输出,避免与无关格式细节耦合,除非格式本身就是契约的一部分。
Studio 和产品 UI 测试
按照用户查找控件的方式定位:使用 getByRole、getByLabel、getByText 和可访问名称。优先使用 toBeDisabled() 和 toBeFocused() 等语义状态匹配器,而不是检查属性。
当存在外部结果时,避免将以下内容作为断言目标:
- CSS 类和库私有属性
- 标签名、包装层级、父级遍历或图标是否存在
- 内部菜单 ID 和描述符对象
- 通过直接调用内部菜单项获得的回调负载
- Hook、上下文、reducer 或模态框状态对象
- 仅用作状态存储的内部 ARIA 值
如果没有语义定位器,应改进可访问性或添加稳定的交互钩子。可以使用 CSS 选择器或测试属性,对第三方或其他不可访问的内部元素执行操作,但断言仍应针对可观察的结果。
Remotion 的视觉例外
不要一概禁止检查样式、几何信息、DOM 输出或像素。在 Remotion 中,视觉输出往往就是产品本身。
对于渲染器、效果、布局、媒体和生成的源代码,以下内容可能属于公共契约:
- 像素、屏幕截图、不透明度、颜色和合成效果
- 几何信息、位置、尺寸和时序
- CSS 行为或 DOM 输出
- 帧、时间戳、解码媒体和渲染产物
- 消费者会接收或编辑的精确生成源代码
在可行时,应测试渲染结果,而不只是中间样式对象。对于边界情况较多的布局算法,针对几何信息的单元测试仍然合适,尤其是在配合一项浏览器测试或视觉测试、证明结果已接入渲染时。
使用真实的自有协作组件
默认使用真实的 Remotion 模块、路由、文件系统操作、服务、组件和浏览器渲染。对于 Remotion 不拥有的边界,例如 AWS、不可用的远程服务、时间或非确定性的外部依赖,则使用模拟或伪造对象。
不要为了简化设置而模拟内部生产模块或相邻的自有软件包。如果无法避免使用内部伪对象,应说明为什么真实边界不切实际,并确保由另一项测试覆盖该伪对象绕过的接线逻辑。
良好的集成测试可以使用临时目录、真实的样例项目、真实的服务路由、真实的软件包入口点或真实的媒体文件,同时仅替换外部提供方。
编写更少但覆盖完整工作流的测试
围绕连贯的手动工作流设计测试:一次设置,之后根据工作流需要执行任意数量的操作和断言。
- 在一个测试中包含多个断言是合理的。
- 确保每个测试都能独立运行且彼此隔离。
- 不要将一个工作流拆分为按顺序执行的多个测试,也不要使用共享可变变量或
beforeAll步骤。 - 不要为单个断言重复启动同一个浏览器、渲染器或 Studio 进程。
- 如果设置、执行者和目标相同,就扩展现有工作流。
- 对真正不同的设置、契约或失败路径,添加单独的测试。
- 对纯函数的表格类边界情况,使用表驱动单元测试,而不是人为构造浏览器工作流。
- 不要仅为了减少测试文件数量而组合无关行为。
在 packages/example/e2e 中,启动 Studio 的开销很大,而且测试套件会串行运行。构成同一工作流的标题、可见性、导航、交互和最终结果断言,不应各自重启 Studio。应将它们合并为一个可独立运行的测试,而不是通过 beforeAll 共享进程状态。
添加断言前,先确认相邻的、更有力的断言是否已经能证明相同结论。一个有力的结果胜过多个冗余的中间观察。
单元测试的作用
单元测试适用于:
- 有许多有意义输入的纯算法
- 解析器、序列化器、codemod 和协议转换
- 具有组合分支的状态机和错误处理
- 几何与计时运算,其中边界情况会使宽泛测试变得笨重
- 返回值本身就是契约的公共工具函数
对于跨层功能,应先添加或确定主要集成工作流。仅当宽泛测试无法经济地覆盖重要边界情况时,才添加单元测试。
不要仅为了使某个简单辅助函数可测试而将其提取出来或导出。如果辅助函数运行正常,但功能入口点可以停止调用它而测试仍不失败,那么该测试不足以作为主要覆盖。
即使有人要求编写测试,也要避免只断言实现细节的测试。报告其局限性以及进行过的任何手动验证。
避免以下反模式
- 将合并、映射或构建数组的辅助函数测试作为 UI 功能的唯一覆盖
- 断言内部菜单 ID、回调描述符、reducer 状态或 hook 状态
- 在风险实际位于 CLI 接线时直接调用格式化器
- 仅为提高覆盖率而针对每一行变更的生产代码编写测试
- 在单元、路由、组件和 E2E 层重复测试相同行为,却没有体现各层的独特风险
- 当存在更小的契约或最终产物时,对大型内部对象或完整标记树做快照
- 模拟最可能出问题的集成所在模块
- 尽管设置相同,却针对每条需求句子都创建一个测试块
- 只证明没有发生错误,却没有证明预期操作成功
验证回归测试的红绿状态
对于 bug 修复,在实际可行时,验证新测试能检测到回归:
- 在修复存在时运行测试。
- 暂时撤销或断开相关生产代码片段。
- 确认测试因预期原因失败。
- 恢复修复并确认测试通过。
使用以下变异启发式:
如果被测辅助函数仍然有效,但功能入口点不再调用它,这个测试会失败吗?
如果不会,就将主要测试移到更宽的边界。
在最终回复中报告 Red/green verified。如果无法进行验证,请说明原因,不要让人误以为已经完成验证。
示例
新建合成默认值
主要覆盖较弱:
expect(getNewCompositionDefaults(selected)).toEqual(selected);
更强的工作流:
- 选择纵向合成。
- 打开“新建合成”。
- 验证宽度、高度、FPS 和时长均已填入。
- 创建合成。
- 断言生成的合成或源文件包含这些值。
如果能体现有意义的独立逻辑,针对特定场景的单元测试仍可覆盖不常见的回退输入。
CLI 帮助
主要覆盖较弱:
expect(getCreateVideoHelp()).toContain('--help');
更强的集成测试:
- 通过真实的 CLI 入口点执行
create-video --help。 - 断言命令成功退出并输出预期内容。
- 断言没有出现提示,也没有创建项目。
只读 Studio
主要覆盖较弱:
expect(getItem(items, 'rename').disabled).toBe(true);
更强的工作流:
- 以只读模式打开 Studio。
- 执行安全操作,并断言其可见效果或外部影响。
- 验证修改操作不可用,或无法更改源文件。
负向回归
覆盖较弱:
await expect(page.getByText(error)).toBeHidden();
更强的覆盖会执行操作并断言其正向结果,例如新的序列顺序、源文件更改、请求、持久化值或渲染输出。此时可将未出现错误作为次要断言。
仓库示例
可参考以下测试形态和边界选择:
- 完整的 HMR 工作流:
packages/example/e2e/error-overlay.test.mts - 跨边界的监视器工作流:
packages/example/e2e/suppress-rebuild.test.mts - 真实 Player 和媒体视觉测试:
packages/media/src/test/player-preview-frame-accuracy.test.tsx - 带有 Provider 边界伪对象的云端编排:
packages/lambda/src/test/integration/deploy-site-from-bundle.test.ts - 浏览器视觉输出:
packages/web-renderer/src/test/opaque-layer-over-fading-layer.test.tsx
针对子系统机制,请遵循 web-renderer-test 等专用技能,同时保留此技能中的原则。
审查和报告
完成前:
- 确认测试覆盖主要的公共契约或用户可见契约。
- 确认实际功能的连接逻辑失效时,测试会失败。
- 移除冗余断言和重复的低层覆盖。
- 为每个更改的测试运行对应的专项测试命令。
- 条件允许时,进行红绿回归验证。
报告测试更改时,请说明:
- 测试的主要行为或契约
- 选择的边界:单元、集成、视觉/浏览器或 E2E
- 该边界为何能提供有效保障
- 模拟了哪些内部依赖(如有),以及原因
- 是否扩展了现有工作流;若新建场景,请说明原因
已验证红绿,或说明为何无法实际进行
原则改编自: