agent-skills 测试模式参考详解:JS/TS 测试结构、命名、Mock 纪律与反模式实战
references/testing-patterns.md 是 agent-skills 仓库中面向 JavaScript/TypeScript 生态的测试模式快速参考(Quick Reference),它把 test-driven-development 技能中定义的四条通用原则——Arrange-Act-Assert 结构、描述性命名、Mock 纪律和反模式清单——落到了 Jest、React Testing Library、Supertest 与 Playwright 的具体语法上。读完本篇,你不仅能直接复制其中的断言、Mock 与组件/API/E2E 测试写法,还能对照仓库中的真实 TDD 评估工程(evals/fixtures/test-driven-development/)与评估用例(evals/cases/test-driven-development.json),理解这套模式在"AI Agent 驱动开发"场景下的完整落地方式。
一、定位:它是 TDD 技能的"语法层"参考
在 agent-skills 中,每个技能(skill)负责"怎么做"的通用原则,而 references/ 目录下的参考清单(Reference Checklists)则是技能按需拉取的速查材料。testing-patterns.md 在 README.md 的参考清单表中的定位是:
Reference Covers testing-patterns.md Test structure, naming, mocking, React/API/E2E examples, anti-patterns (JavaScript/TypeScript)
skills/test-driven-development/SKILL.md 的 "See Also" 一节(约 L361)明确指向该文件,并给出了一段关键的适用边界声明:
原则(Arrange-Act-Assert、命名、mock 纪律、反模式)适用于任何生态;这里展示的语法与工具是 JS/TS 专属的。在其他技术栈中,遵循相同的原则,使用该仓库自己的测试框架与命令即可。
docs/getting-started.md 中的技能-参考映射表也把 testing-patterns.md 绑定到 test-driven-development 技能。因此理解这篇参考的正确姿势是:原则层在 TDD 技能里,语法层在本参考里。下面按原文档的章节顺序完整展开每个模式,并结合仓库内可运行的测试工程补充佐证。
二、测试结构:Arrange-Act-Assert
每个测试体内部按"三幕"组织:Arrange(准备测试数据与前置条件)→ Act(执行被测动作)→ Assert(验证结果):
it('describes expected behavior', () => {
// Arrange: Set up test data and preconditions
const input = { title: 'Test Task', priority: 'high' };
// Act: Perform the action being tested
const result = createTask(input);
// Assert: Verify the outcome
expect(result.title).toBe('Test Task');
expect(result.priority).toBe('high');
expect(result.status).toBe('pending');
});
这一模式与 skills/test-driven-development/SKILL.md 中 "Use the Arrange-Act-Assert Pattern" 一节的示例是同一来源,只是本参考更强调注释标记三个阶段的边界,便于在长测试中快速定位"我到底在验证哪一步"。
三、命名约定:测试名即规格说明
命名模式为 [单元] [预期行为] [条件]:
// Pattern: [unit] [expected behavior] [condition]
describe('TaskService.createTask', () => {
it('creates a task with default pending status', () => {});
it('throws ValidationError when title is empty', () => {});
it('trims whitespace from title', () => {});
it('generates a unique ID for each task', () => {});
});
注意两个要点:
describe块命名精确到模块.方法(TaskService.createTask),而不是笼统的模块名——TDD 技能的 Red Flags 清单中明确把"测试名不描述预期行为"列为红旗项;it描述句是"行为 + 条件"的陈述句,读者不读实现也能知道该测试验证什么。agents/test-engineer.md 中"6. Every test name should read like a specification(每个测试名都应当读起来像一条规格)"与此完全一致。
四、常用断言速查(Jest Matchers)
原文档给出了一组按类别分组的断言速查,完整继承如下,并补充了各断言的语义要点:
// Equality
expect(result).toBe(expected); // 严格相等 (===)
expect(result).toEqual(expected); // 深度相等(对象/数组)
expect(result).toStrictEqual(expected); // 深度相等 + 类型匹配
// Truthiness
expect(result).toBeTruthy();
expect(result).toBeFalsy();
expect(result).toBeNull();
expect(result).toBeDefined();
expect(result).toBeUndefined();
// Numbers
expect(result).toBeGreaterThan(5);
expect(result).toBeLessThanOrEqual(10);
expect(result).toBeCloseTo(0.3, 5); // 浮点数(精度到小数点后 5 位)
// Strings
expect(result).toMatch(/pattern/);
expect(result).toContain('substring');
// Arrays / Objects
expect(array).toContain(item);
expect(array).toHaveLength(3);
expect(object).toHaveProperty('key', 'value');
// Errors
expect(() => fn()).toThrow();
expect(() => fn()).toThrow(ValidationError);
expect(() => fn()).toThrow('specific message');
// Async
await expect(asyncFn()).resolves.toBe(value);
await expect(asyncFn()).rejects.toThrow(Error);
三个最容易被用错的点:
toBe用===比较引用,只适合原始值;对象/数组断言应使用toEqual(深度相等),toStrictEqual还会校验undefined属性与原型链,适合对类型敏感的场景;toBeCloseTo(0.3, 5)的第二个参数是"允许的位数差",数值越小精度要求越宽松——涉及浮点比较时必须用它而不是toBe;- 异步断言必须
await。原参考反模式表的最后一行专门指出"no async error handling / Swallowed errors, false passes(吞掉的错误导致假通过)",因此异步场景下resolves/rejects两个断言不可省略await。
五、Mock 模式:只在边界处 Mock
5.1 Mock 函数
const mockFn = jest.fn();
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue({ data: 'test' });
mockFn.mockImplementation((x) => x * 2);
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenCalledTimes(3);
mockReturnValue用于同步固定返回;mockResolvedValue用于把异步函数"钉"成返回固定数据的 Promise;mockImplementation则给出带逻辑的替身;- 交互断言(
toHaveBeenCalled系列)只应在验证"外部副作用确实发生了"时使用——这与 TDD 技能"Test State, Not Interactions(测状态,不测交互)"的原则相衔接:默认断言结果状态,而不是内部调用序列。
5.2 Mock 模块
// Mock an entire module
jest.mock('./database', () => ({
query: jest.fn().mockResolvedValue([{ id: 1, title: 'Test' }]),
}));
// Mock specific exports
jest.mock('./utils', () => ({
...jest.requireActual('./utils'),
generateId: jest.fn().mockReturnValue('test-id'),
}));
第二种写法是实用技巧:用 jest.requireActual 先展开真实模块,再只覆盖需要隔离的导出(此处仅替换 generateId),其余导出保持真实行为——这比整模块 Mock 更接近"最小替身"原则。
5.3 边界 Mock 清单:Mock 什么、不 Mock 什么
原文档给出了一张决策对照表,这是 Mock 纪律的核心:
Mock these: Don't mock these:
├── Database calls ├── Internal utility functions
├── HTTP requests ├── Business logic
├── File system operations ├── Data transformations
├── External API calls ├── Validation functions
└── Time/Date (when needed) └── Pure functions
判断依据可以从 skills/test-driven-development/SKILL.md 的 "Prefer Real Implementations Over Mocks" 一节补全为四级优先序:
1. Real implementation → 置信度最高,能抓到真实 bug
2. Fake → 依赖的内存版实现(如内存数据库)
3. Stub → 只返回固定数据、无行为
4. Mock (interaction) → 验证方法调用—— sparingly(慎用)
只有当真实实现"太慢、非确定性、或存在不可控副作用(外部 API、发邮件)"时才允许 Mock。agents/test-engineer.md 的规则 5 同样写道:"Mock at system boundaries (database, network), not between internal functions(在系统边界处 Mock,而不是在内部函数之间)"。
六、React / 组件测试(React Testing Library)
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
describe('TaskForm', () => {
it('submits the form with entered data', async () => {
const onSubmit = jest.fn();
render(<TaskForm onSubmit={onSubmit} />);
// Find elements by accessible role/label (not test IDs)
await screen.findByRole('textbox', { name: /title/i });
fireEvent.change(screen.getByRole('textbox', { name: /title/i }), {
target: { value: 'New Task' },
});
fireEvent.click(screen.getByRole('button', { name: /create/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({ title: 'New Task' });
});
});
it('shows validation error for empty title', async () => {
render(<TaskForm onSubmit={jest.fn()} />);
fireEvent.click(screen.getByRole('button', { name: /create/i }));
expect(await screen.findByText(/title is required/i)).toBeInTheDocument();
});
});
这段示例里有三条值得刻进习惯的规则:
- 按可访问性角色定位元素(
getByRole('textbox', { name: /title/i })),注释明确写着 "not test IDs"。用data-testid或class定位属于"测实现细节",重构即碎;按角色/标签定位则等价于以"用户视角"驱动组件,且天然覆盖可访问性回归; - 对 props 回调做最小 Mock(
onSubmit = jest.fn())——组件测试的边界就是 props,而不是组件内部状态机; - 异步用
waitFor/findBy*处理渲染时序,避免轮询式 sleep。这与第四节的异步断言纪律一脉相承。
第二个用例还演示了"负路径测试"的写法:触发空标题提交,断言错误文案出现——对应 agents/test-engineer.md 中要求每类函数覆盖的"Happy path / Empty input / Boundary values / Error paths / Concurrency"五类场景中的后几类。
七、API / 集成测试(Supertest)
import request from 'supertest';
import { app } from '../src/app';
describe('POST /api/tasks', () => {
it('creates a task and returns 201', async () => {
const response = await request(app)
.post('/api/tasks')
.send({ title: 'Test Task' })
.set('Authorization', `Bearer ${testToken}`)
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
title: 'Test Task',
status: 'pending',
});
});
it('returns 422 for invalid input', async () => {
const response = await request(app)
.post('/api/tasks')
.send({ title: '' })
.set('Authorization', `Bearer ${testToken}`)
.expect(422);
expect(response.body.error.code).toBe('VALIDATION_ERROR');
});
it('returns 401 without authentication', async () => {
await request(app)
.post('/api/tasks')
.send({ title: 'Test' })
.expect(401);
});
});
要点解读:
request(app)直接以应用实例发起请求,走的是真实中间件/路由/序列化管线,属于 TDD 技能资源模型中的 Medium 级测试(localhost、秒级、可带测试数据库),无需起容器;- 三个用例恰好覆盖一个接口的三条契约:成功路径(201 + 响应体结构)、校验失败(422 + 错误码
VALIDATION_ERROR)、未认证(401)。注意 201 用例中对动态id使用expect.any(String)——只断言类型不断言具体值,这正是"断言结果状态、但不断言无法稳定承诺的实现细节"的正确取舍; toMatchObject做部分结构匹配,允许接口新增字段而不必改测试,属于"宽松但不越界"的断言风格。
八、E2E 测试(Playwright)
import { test, expect } from '@playwright/test';
test('user can create and complete a task', async ({ page }) => {
// Navigate and authenticate
await page.goto('/');
await page.getByRole('textbox', { name: /email/i }).fill('test@example.com');
await page.getByLabel(/password/i).fill('testpass123');
await page.getByRole('button', { name: /log in/i }).click();
// Create a task
await page.getByRole('button', { name: /new task/i }).click();
await page.getByRole('textbox', { name: /title/i }).fill('Buy groceries');
await page.getByRole('button', { name: /create/i }).click();
// Verify task appears
const task = page.getByRole('listitem', { name: /buy groceries/i });
await expect(task).toBeVisible();
// Complete the task
await task.getByRole('checkbox', { name: /complete buy groceries/i }).check();
await expect(task).toHaveCSS('text-decoration-line', 'line-through');
});
按 TDD 技能的测试金字塔,E2E 只占约 5%,应限制在关键用户路径上;这个用例就是典型的"登录 → 创建 → 完成"关键链路。写法上延续了前几节的纪律:
- 全程用
getByRole/getByLabel语义定位器,没有任何 CSS 选择器或data-testid; - 断言混合了可见性(
toBeVisible)与视觉状态(toHaveCSS('text-decoration-line', 'line-through'))——完成态用"删除线"这类用户可感知的外观变化来验证,而不是去查 DOM 里的某个内部类名; - 注意与组件测试的分工:表单交互逻辑已在第六节的组件测试中覆盖,E2E 只负责把整条链路(含认证、路由、真实浏览器渲染)串起来验证一次,不重复单元测试已覆盖的细节。
九、测试反模式清单(完整表格)
原文档以一张三列表格收尾,逐条给出"反模式 → 危害 → 更好的做法",完整继承如下:
| 反模式 | 问题 | 更好的做法 |
|---|---|---|
| Testing implementation details | Breaks on refactor | Test inputs/outputs |
| Snapshot everything | No one reviews snapshot diffs | Assert specific values |
| Shared mutable state | Tests pollute each other | Setup/teardown per test |
| Testing third-party code | Wastes time, not your bug | Mock the boundary |
| Skipping tests to pass CI | Hides real bugs | Fix or delete the test |
Using test.skip permanently |
Dead code | Remove or fix it |
| Overly broad assertions | Doesn't catch regressions | Be specific |
| No async error handling | Swallowed errors, false passes | Always await async tests |
这八条与 skills/test-driven-development/SKILL.md 的 "Test Anti-Patterns to Avoid" 表互为补充:技能侧重"实现细节测试、Flaky 测试、无隔离、过度 Mock"等流程性反模式,本参考则补充了 test.skip 滥用、CI 跳过、异步未 await 这类在具体语法层最容易踩的坑。两份清单合起来即是一份可执行的测试审查核对表。
十、仓库佐证:在 TDD 评估工程里验证这套模式
agent-skills 并非只谈原则——evals/fixtures/ 目录内置了一批可运行的"沙箱项目",其中 evals/fixtures/test-driven-development/ 最能体现上述模式如何被真实使用。
10.1 一个带真实 bug 的最小 Node 工程
该工程是一个名为 split-payment 的分账工具,package.json 声明的测试命令是 Node 内置 runner("test": "node --test"),而不是 Jest——这恰好印证了本参考开篇的声明:原则通用,工具栈因仓库而异,先发现本仓库的测试命令再动手(TDD 技能 "Discover the Stack First" 一节的要求)。
- 被测代码 src/split.js 当前实现有明显缺陷:
function splitCents(totalCents, n) {
const share = Math.floor(totalCents / n);
return Array.from({ length: n }, () => share);
}
Math.floor 直接丢弃余数,余数没有分给任何一份。
-
既有测试 test/split.test.js 只覆盖了整除与单参与者两种"整除型"场景(用
node:test+assert/strict的deepEqual,断言的是输出值而非内部调用——符合"测状态不测交互"); -
BUG.md 记录了财务对账工单 FIN-482:
splitCents(10000, 3)期望[3334, 3333, 3333],实际得到[3333, 3333, 3333](合计 9999,丢一分钱); -
README.md 定义了两条必须对所有输入成立的不变量:Exactness(各份之和恰等于总额)与 Fairness(任意两份相差不超过 1 分,余数按顺序每份 1 分),并给出标准答案
splitCents(100, 7)应为[15, 15, 14, 14, 14, 14, 14]。
10.2 评估用例如何"验收"这些原则
evals/cases/test-driven-development.json 为这个工程定义了三个评估用例,其 expectations 字段几乎逐条对应本文前面各节的模式要求:
- Prove-It 时序:
A test reproducing the lost-cent case from BUG.md is added and shown failing before src/split.js is modified—— 修复前必须先有"可复现且失败"的测试。这正是 TDD 技能 Prove-It Pattern 流程图(复现测试 → 确认失败 → 实现修复 → 确认通过 → 跑全量套件)的可执行版本; - 不变量要有自己的测试:
The fairness invariant from the README has its own test case ... on an input with remainder of at least 2 (such as splitCents(100, 7))—— 即第四节强调的"断言具体值、覆盖边界输入":splitCents(100, 7)是检验"余数均分给最早的份额"还是"余数全堆给一份"的判别输入; - 全量套件用本仓库命令跑:
The full suite is run with the repository's own command after the fix—— 用例 3 甚至专门考察跨生态场景(Python 工程里必须用python3 -m unittest而不是npm test)。
另一个佐证在 evals/fixtures/debugging-and-error-recovery/pagination.test.js:同样是 node:test + 描述性测试名('returns the second page for a one-based page number')+ 对返回值做 deepEqual 的结构断言——与第三节命名约定和第五节"状态断言优先"完全同构。
10.3 使用入口:/test 命令与 test-engineer 角色
在仓库的消费端,这套参考通过两条路径被 Agent 实际调用:
- 斜杠命令 commands/test.toml:
/test会触发 TDD 工作流——新功能走"写失败测试 → 实现 → 重构",修 bug 走 Prove-It 五步(复现测试 → 确认失败 → 修复 → 确认通过 → 全量回归); - 测试工程角色 agents/test-engineer.md:QA 人设,规则集与本参考一致(行为优先、每测一概念、测试独立、慎用快照、只 Mock 边界、测试名如规格),并规定其覆盖"快乐路径 / 空输入 / 边界值 / 错误路径 / 并发"五类场景。
十一、落地小结:把这份参考当作审查清单
把 references/testing-patterns.md 与 TDD 技能放在一起使用,可以得到一套可直接执行的检查流程:
- 写之前:确认本仓库的测试命令与既有测试的目录/命名约定(不要假设
npm test); - 写的时候:每个测试内部 Arrange-Act-Assert 三段分明;
describe到模块.方法、it写成"行为 + 条件";组件测试按角色定位元素,API 测试覆盖 201/422/401 三类契约,E2E 只留给关键用户链路; - Mock 检查:对照"Mock these / Don't mock these"清单与四级替身优先序,凡能在边界内用真实实现或 Fake 解决的,不要动用交互 Mock;
- 提交前:过一遍九节那张八条反模式表,加上 TDD 技能的 Red Flags 清单(无对应测试的新代码、首跑即通过的"测试"、跳过测试让 CI 变绿等);
- 收尾:用仓库自己的命令跑完整测试套件,且只改代码后才需要重跑——干净跑完后重复执行同一命令并不增加置信度(TDD 技能 Verification 一节的备注)。
需要再次强调的适用边界:本参考中所有 jest.*、@testing-library/react、supertest、@playwright/test 语法均面向 JavaScript/TypeScript 技术栈;若项目使用 Go、Rust、Python 或其他框架,应保留同样的结构、命名、Mock 纪律与反模式标准,替换为该生态的测试框架与命令。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00