关于 contract-testing-ts
contract-testing-ts 是为 TypeScript 项目配置和编写声明式契约测试的技能。它通过 Vitest 使用 `*.contract.yaml` 测试函数、类和对象,并指导验证测试结果及模拟依赖。
主要功能
- 01配置 Vitest 契约测试发现
- 02为导出项编写 YAML 契约
- 03覆盖输入和错误场景
- 04检查模拟依赖的测试项
- 05运行并验证契约测试
适用场景
- 01为软件包添加契约测试
- 02为函数或类创建契约
- 03测试边界情况和错误行为
- 04验证使用模拟依赖的契约
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ beecode-rs/ msh-test-contractor --skill contract-testing-ts安装后在 Claude Code 中输入/contract-testing-ts确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/contract-structure.mdexamples.mdmocking.mdspecial-objects.mdterm-types.md详细介绍
Test Contractor — 基于契约的测试
你正在协助使用 test-contractor 库:这是一个基于契约的测试框架,可通过 *.contract.yaml 文件以声明方式定义测试用例。你将处理两种主要工作流:
- 项目设置 — 配置软件包,通过 Vitest 查找并运行
*.contract.yaml文件 - 编写契约 — 为函数、类和对象创建并维护契约 YAML 文件
这两种工作流都需要通过运行契约测试来验证工作成果。
如何判断使用哪种工作流
留意用户的需求:
- “设置契约测试”、“配置契约”、“添加 test:contract 脚本”、“我想在这个项目中使用契约测试” → 转到项目设置
- “为……编写测试”、“编写……的契约”、“为这个文件添加契约”,或用户指出了源文件 → 转到编写契约
- “为这些文件编写契约,并确保项目已完成设置” → 两者都做,并从设置开始
- 如果需求不明确,请询问用户
提问
遇到不清楚的地方时,先询问再继续。以下是一些常见的情况:
- 用户指出了一个文件,但未说明要测试哪些导出项 → 询问他们希望覆盖哪些函数或类
- 源文件有复杂的依赖关系,且不清楚哪些依赖应该模拟 → 询问要隔离哪些依赖
- 用户希望为某个类编写契约,但没有提到构造函数场景 → 询问他们是否也需要构造函数相关的条款
- 你发现有多个可能的
module路径(例如,可以从不同位置导入该文件)→ 确认正确的路径 - 用户笼统地提到“测试” → 确认他们指的是 contract.yaml 还是 Vitest 单元测试
不要询问那些可以通过阅读代码确定的事项。只有在选择确实会影响输出时才提问。
项目设置
每个软件包只需设置一次。完成后,npm run test:contract 会自动查找并运行所有 *.contract.yaml 文件。
步骤 1:检查是否已完成设置
进行更改之前,检查项目是否已经有:
vitest.config.contract.ts文件package.json中的test:contract脚本
如果两者都存在,说明项目已完成配置——跳转到编写契约,或询问用户需要什么帮助。
步骤 2:创建 vitest.config.contract.ts
将此文件放在项目根目录下(与 package.json 位于同一目录):
import { ContractReporter } from '@beecode/msh-test-contractor/contract-reporter'
import { contractYamlPlugin } from '@beecode/msh-test-contractor/vitest-plugin'
import tsconfigPaths from 'vite-tsconfig-paths'
import { coverageConfigDefaults, defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [tsconfigPaths(), contractYamlPlugin()],
test: {
coverage: {
exclude: ['lib/**', 'src/index.ts', 'src/**/__fixtures__/**', ...coverageConfigDefaults.exclude],
},
exclude: ['src/**/__fixtures__/**'],
include: ['src/**/*.contract.yaml'],
mockReset: true,
passWithNoTests: true,
reporters: [new ContractReporter()],
setupFiles: ['./src/__tests__/index-vitest-setup.ts'],
watch: false,
},
})
如果项目结构与常规布局不同,请相应调整路径。关键字段:
include— 用于查找*.contract.yaml文件的 glob 模式。如果契约文件位于其他位置,请调整此项。setupFiles— 可选;用于设置测试环境的文件(例如,process.env.TZ = 'utc')。如果不需要,请移除。ContractReporter— 格式化契约测试输出,方便快速查看。如果你更喜欢 Vitest 的默认格式,也可以使用'verbose'。
如果项目尚未依赖 vite-tsconfig-paths,则需要安装:
npm install --save-dev vite-tsconfig-paths
步骤 3:在 package.json 中添加 test:contract 脚本
{
"scripts": {
"test:contract": "vitest --config=./vitest.config.contract.ts"
}
}
如果已有一个运行多个测试套件的 test 脚本(例如,"test": "concurrently --group 'npm:test:*'"),那么该脚本会自动调用 test:contract。
步骤 4:验证设置
npm run test:contract
如果还没有契约文件,你会看到“no test files found”——这是预期结果。设置已完成。
编写契约
工作流概述
- 阅读用户希望测试的源文件
- 确认导出项及其签名
- 在源文件旁创建
*.contract.yaml文件 - 检查模拟的依赖是否缺少条款(参见模拟依赖检查)
- 运行契约以验证它们能否通过
- 修复任何失败项并重新运行
何时阅读参考文件
根据需要按需阅读以下文件:
| 文件 | 阅读时机 |
|---|---|
references/contract-structure.md | 始终阅读——核心结构、必填字段、主体类型 |
references/term-types.md | 编写包含基本类型、对象、数组或错误的参数/结果时 |
references/special-objects.md | 使用 !undefined、new Error(...)、new Date(...)、Promise.resolve(...)、__fn__、__class_ref:、__import: 时 |
references/mocking.md | 主体具有需要模拟的依赖项时 |
references/examples.md | 需要展示源代码与契约配对的完整端到端示例时 |
始终从阅读 references/contract-structure.md 开始——它定义了必填字段和约定。根据具体情况阅读其他文件。
契约文件放置位置
将 *.contract.yaml 文件放在与源文件相同的目录中。module 字段必须指向编译后的 .js 文件,而不是 .ts 源文件。
src/
user-service.ts ← source
user-service.contract.yaml ← contract (module: ./user-service.js)
项设计策略:零、一、多、大量、糟糕情况
为方法编写项时,按顺序考虑以下五种情况。每个方法以 3–7 个项为目标。
| 场景 | 测试内容 | 示例 |
|---|---|---|
| 零 | 空输入/缺失输入。什么都没有时会怎样? | []、''、null、{}、0 |
| 一 | 单个项目。最简单的非平凡情况。 | [42]、['alice']、{ id: 1 } |
| 多 | 少量项目。典型/理想路径情况。 | [1, 2, 3]、包含 3–5 个元素的数组 |
| 大量 | 大型输入。行为会随规模变化吗? | 包含 1000 个项目的数组、非常长的字符串 |
| 糟糕情况 | 出错情况。无效输入、边界情况、错误。 | 负 ID、格式错误的对象、类型错误 |
并非每个方法都需要涵盖全部五种情况——不适用的场景可以跳过。阅读源代码后,对每个导出的方法问自己:
- 对此函数来说,“什么都没有”是什么样的?→ 零
- 最简单且有意义的输入是什么?→ 一
- 正常、典型的调用是什么样的?→ 多
- 函数的行为会随规模变化吗?→ 大量(不适用时跳过)
- 可能出现什么问题?→ 糟糕情况(错误、边界情况、临界值)
然后为每个适用的场景编写一个项。
模拟依赖检查
如果你编写的契约使用 mock 引用了另一个单元的契约,就需要验证被引用的契约是否覆盖了被测对象实际发起的调用。这一点很重要:如果被测对象使用的参数与模拟契约中的任何条款都不匹配,模拟就不知道该返回什么,测试可能会失败或表现异常。
检查流程:
- 编写契约后,阅读被测对象的源代码,找出所有对模拟依赖的调用
- 对于每个调用,检查模拟依赖的契约中是否有
params与被测对象传入的参数匹配的条款 - 如果源代码中存在调用,但模拟契约中没有匹配的条款,则添加缺失的条款
- 更新后,重新运行模拟依赖自身的契约测试,确保新条款有效
**示例:**如果 orderService.createOrder({ item: 'book', price: 29.99 }) 内部调用了 logger.debug('Creating order: book'),那么 logger.contract.yaml 必须包含一条针对 debug 的条款,其 params: ['Creating order: book'](或能匹配该内容的模式)。如果缺少这条款,就添加它。
这项检查可以避免一种常见故障:契约单独运行时通过,但被另一个契约用作模拟时却失败。
验证契约
编写或更新契约后,务必运行:
npm run test:contract
如果某条款失败:
- 仔细阅读错误消息,它会指出失败的条款以及实际结果与预期结果
- 检查
params是否与函数实际收到的参数匹配 - 检查
result是否与函数实际返回的结果匹配 - 如果使用了模拟,确认模拟条款覆盖了被测对象发起的所有调用
- 修复条款并重新运行
如果你更新了模拟依赖的契约,也要运行它的契约测试:
npx vitest --config=./vitest.config.contract.ts path/to/the-updated.contract.yaml
最简模板
subject: myFunction # exported name
module: ./my-module.js # path to compiled JS
subjectType: function # or "class"
methods:
__self__: # for standalone functions; use method name for objects
terms:
- params: [input]
result: expected
回退到 Vitest
Contract.yaml 涵盖了大多数测试场景。只有在无法通过声明式方式表达的情况下,才使用 Vitest *.test.ts:
- 断言内部调用的顺序或次数(例如,“helper 恰好被调用两次”)
- 无法静态设置的运行时设置或清理(例如,临时文件、网络服务器)
- 非确定性状态(例如,无法通过契约模拟的时间戳)
- 复杂的异步模式(延迟重试、竞态条件、流式处理)
遇到这类情况时,只针对该特定情况编写 Vitest 测试,其余部分仍放在 contract.yaml 中。在文件顶部添加注释:
// Supplements: ./my-module.contract.yaml
// Covers: [what's tested here and why contract.yaml couldn't handle it]
快速参考
- 文件命名:
*.contract.yaml(例如,user-service.contract.yaml) subject: 导出名称,必须与其在模块中的名称完全一致module: 已编译.js文件的相对路径(例如,./user-service.js)__self__: 独立函数的方法名constructor+constructorParams: 用于类被测对象- 每条条款: 输入
params,输出result或error - 自动发现: Vite 插件会找到所有
*.contract.yaml文件,无需入口.test.ts - 运行:
npm run test:contract(需要设置) - 条款策略: 零、一、多、很多、出错