/unit-testing
单元测试方法论,覆盖 AAA 模式、Test Doubles、跨语言示例(JS/TS、Python、Go、Java)、前后端单元测试、边界与属性测试、变异测试
Claude Code
$ npx -y skills add echoVic/boss-skill --skill unit-testing --agent claude-code
Codex
$ npx skills add echoVic/boss-skill --skill unit-testing -a codex -y
Cursor
$ npx skills add echoVic/boss-skill --skill unit-testing -a cursor -y
Gemini CLI
$ npx skills add echoVic/boss-skill --skill unit-testing -a gemini-cli -y
GitHub Copilot
$ npx skills add echoVic/boss-skill --skill unit-testing -a github-copilot -y
OpenCode
$ npx skills add echoVic/boss-skill --skill unit-testing -a opencode -y
Windsurf
$ npx skills add echoVic/boss-skill --skill unit-testing -a windsurf -y
SKILL.md
unit-testing.SKILL.mdname: qa/unit-testing
description: 单元测试方法论,覆盖 AAA 模式、Test Doubles、跨语言示例(JS/TS、Python、Go、Java)、前后端单元测试、边界与属性测试、变异测试
version: 1.0.0
agent: qa
type: methodology
user-invocable: false
agent-invocable: true
dependencies:
- shared/tech-stack-detection
triggers:
- 需要编写或审查单元测试时
- 需要测试纯函数、Service 层、组件、Hook 时
- 需要确定 Mock 边界时
metadata:
internal: true
单元测试方法论
单元测试的定义与原则
单元测试验证**最小可测试单元**(函数、方法、类、组件)的行为,隔离外部依赖。
核心原则
| 原则 | 说明 | | -------- | ------------------------------------------------ | | **隔离** | 一个测试只验证一个行为,不依赖其他测试或外部系统 | | **快速** | 毫秒级执行,全部单元测试应在秒级完成 | | **确定** | 相同输入多次运行结果一致,不依赖时间/网络/随机数 | | **可读** | 测试代码即文档,命名清晰表达意图 | | **独立** | 测试顺序不影响结果,可任意顺序执行 |
什么该做单元测试
| 应该测 | 不应该测 | | ------------------------ | ------------------------------ | | 纯函数 / 工具函数 | 框架内部行为(React 渲染机制) | | Service 层业务逻辑 | 第三方库本身 | | 组件渲染与交互(前端) | 配置文件 / 常量 | | Hook 状态逻辑 | 简单的 getter/setter | | 校验 / 转换 / 格式化逻辑 | 数据库查询(属于集成测试) | | 状态机 / 状态转换 | 网络请求(属于集成测试) |
AAA 模式(Arrange-Act-Assert)
每个单元测试遵循三段式结构:
it('applies 10% discount when total > 100', () => {
// Arrange — 准备测试数据和环境
const items = [{ price: 60, quantity: 2 }]; // total = 120
const discount = 0.1;
// Act — 执行被测逻辑
const result = calculateTotal(items, discount);
// Assert — 验证结果
expect(result).toBe(108); // 120 * 0.9
});**命名规范**:`[期望行为] when [条件]` 或 `[动作] [结果]`
// ✅ 好
it('returns error when email is invalid');
it('calculates total with discount applied');
// ❌ 差
it('should work');
it('test1');Test Doubles(测试替身)
根据用途选择不同的替身类型:
| 类型 | 用途 | 示例 | | --------- | -------------------- | --------------------------------------- | | **Dummy** | 占位,不被调用 | 传一个空对象满足参数签名 | | **Stub** | 提供预设返回值 | `repo.findById.mockResolvedValue(user)` | | **Spy** | 记录调用信息 | `expect(fn).toHaveBeenCalledWith(arg)` | | **Mock** | Stub + Spy,验证交互 | `mock.calledWith(...)` | | **Fake** | 简化的真实实现 | 内存数据库替代真实 DB |
何时使用哪种
// Stub:被测代码需要依赖返回值,但不关心调用细节
jest.mock('../repo', () => ({
findById: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
}));
// Spy:需要验证依赖是否被正确调用
const emailSpy = jest.spyOn(emailService, 'send');
await userService.register(user);
expect(emailSpy).toHaveBeenCalledWith(user.email, expect.any(String));
// Fake:需要一个可用但简化的实现
class FakeCache implements Cache {
private store = new Map<string, string>();
async get(k: string) {
return this.store.get(k) ?? null;
}
async set(k: string, v: string) {
this.store.set(k, v);
}
}前端单元测试
组件渲染测试
// Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
describe('Button', () => {
it('renders children text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('calls onClick when clicked', async () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click</Button>);
await userEvent.click(screen.getByRole('button', { name: 'Click' }));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('does not call onClick when disabled', async () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick} disabled>Click</Button>);
await userEvent.click(screen.getByRole('button', { name: 'Click' }));
expect(handleClick).not.toHaveBeenCalled();
});
it('applies variant styles', () => {
const { container } = render(<Button variant="primary">Save</Button>);
expect(container.firstChild).toHaveClass('btn-primary');
});
});Hook 测试
// useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('initializes with default value 0', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('initializes with provided value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('increments count', () => {
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
it('decrements count but not below 0', () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.decrement());
expect(result.current.count).toBe(0); // 边界:不低于 0
});
it('resets to initial value', () => {
const { result } = renderHook(() => useCounter(5));
act(() => result.current.increment());
act(() => result.current.reset());
expect(result.current.count).toBe(5);
});
});工具函数测试
// formatDate.test.ts
import { formatDate, formatCurrency, maskEmail } from './format';
describe('formatDate', () => {
it('formats ISO date to YYYY-MM-DD', () => {
expect(formatDate('2024-01-15T10:30:00Z')).toBe('2024-01-15');
});
it('handles invalid date gracefully', () => {
expect(formatDate('invalid')).toBe('');
});
});
describe('maskEmail', () => {
it('masks local part of email', () => {
expect(maskEmail('alice@example.com')).toBe('a***@example.com');
});
it('handles short local part', () => {
expect(maskEmail('ab@example.com')).toBe('a*@example.com');
});
it('returns empty for invalid email', () => {
expect(maskEmail('not-an-email')).toBe('');
});
});后端单元测试
Service 层测试(Mock Repository)
// userService.test.ts
import { UserService } from './userService';
// Mock 整个Read more
name: qa/unit-testing description: 单元测试方法论,覆盖 AAA 模式、Test Doubles、跨语言示例(JS/TS、Python、Go、Java)、前后端单元测试、边界与属性测试、变异测试 version: 1.0.0 agent: qa type: methodology user-invocable: false agent-invocable: true dependencies: - shared/tech-stack-detection triggers: - 需要编写或审查单元测试时 - 需要测试纯函数、Service 层、组件、Hook 时 - 需要确定 Mock 边界时 metadata: internal: true
单元测试方法论
单元测试的定义与原则
单元测试验证**最小可测试单元**(函数、方法、类、组件)的行为,隔离外部依赖。
核心原则
| 原则 | 说明 | | -------- | ------------------------------------------------ | | **隔离** | 一个测试只验证一个行为,不依赖其他测试或外部系统 | | **快速** | 毫秒级执行,全部单元测试应在秒级完成 | | **确定** | 相同输入多次运行结果一致,不依赖时间/网络/随机数 | | **可读** | 测试代码即文档,命名清晰表达意图 | | **独立** | 测试顺序不影响结果,可任意顺序执行 |
什么该做单元测试
| 应该测 | 不应该测 | | ------------------------ | ------------------------------ | | 纯函数 / 工具函数 | 框架内部行为(React 渲染机制) | | Service 层业务逻辑 | 第三方库本身 | | 组件渲染与交互(前端) | 配置文件 / 常量 | | Hook 状态逻辑 | 简单的 getter/setter | | 校验 / 转换 / 格式化逻辑 | 数据库查询(属于集成测试) | | 状态机 / 状态转换 | 网络请求(属于集成测试) |
AAA 模式(Arrange-Act-Assert)
每个单元测试遵循三段式结构:
it('applies 10% discount when total > 100', () => {
// Arrange — 准备测试数据和环境
const items = [{ price: 60, quantity: 2 }]; // total = 120
const discount = 0.1;
// Act — 执行被测逻辑
const result = calculateTotal(items, discount);
// Assert — 验证结果
expect(result).toBe(108); // 120 * 0.9
});**命名规范**:`[期望行为] when [条件]` 或 `[动作] [结果]`
// ✅ 好
it('returns error when email is invalid');
it('calculates total with discount applied');
// ❌ 差
it('should work');
it('test1');Test Doubles(测试替身)
根据用途选择不同的替身类型:
| 类型 | 用途 | 示例 | | --------- | -------------------- | --------------------------------------- | | **Dummy** | 占位,不被调用 | 传一个空对象满足参数签名 | | **Stub** | 提供预设返回值 | `repo.findById.mockResolvedValue(user)` | | **Spy** | 记录调用信息 | `expect(fn).toHaveBeenCalledWith(arg)` | | **Mock** | Stub + Spy,验证交互 | `mock.calledWith(...)` | | **Fake** | 简化的真实实现 | 内存数据库替代真实 DB |
何时使用哪种
// Stub:被测代码需要依赖返回值,但不关心调用细节
jest.mock('../repo', () => ({
findById: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
}));
// Spy:需要验证依赖是否被正确调用
const emailSpy = jest.spyOn(emailService, 'send');
await userService.register(user);
expect(emailSpy).toHaveBeenCalledWith(user.email, expect.any(String));
// Fake:需要一个可用但简化的实现
class FakeCache implements Cache {
private store = new Map<string, string>();
async get(k: string) {
return this.store.get(k) ?? null;
}
async set(k: string, v: string) {
this.store.set(k, v);
}
}前端单元测试
组件渲染测试
// Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
describe('Button', () => {
it('renders children text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('calls onClick when clicked', async () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click</Button>);
await userEvent.click(screen.getByRole('button', { name: 'Click' }));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('does not call onClick when disabled', async () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick} disabled>Click</Button>);
await userEvent.click(screen.getByRole('button', { name: 'Click' }));
expect(handleClick).not.toHaveBeenCalled();
});
it('applies variant styles', () => {
const { container } = render(<Button variant="primary">Save</Button>);
expect(container.firstChild).toHaveClass('btn-primary');
});
});Hook 测试
// useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('initializes with default value 0', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('initializes with provided value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('increments count', () => {
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
it('decrements count but not below 0', () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.decrement());
expect(result.current.count).toBe(0); // 边界:不低于 0
});
it('resets to initial value', () => {
const { result } = renderHook(() => useCounter(5));
act(() => result.current.increment());
act(() => result.current.reset());
expect(result.current.count).toBe(5);
});
});工具函数测试
// formatDate.test.ts
import { formatDate, formatCurrency, maskEmail } from './format';
describe('formatDate', () => {
it('formats ISO date to YYYY-MM-DD', () => {
expect(formatDate('2024-01-15T10:30:00Z')).toBe('2024-01-15');
});
it('handles invalid date gracefully', () => {
expect(formatDate('invalid')).toBe('');
});
});
describe('maskEmail', () => {
it('masks local part of email', () => {
expect(maskEmail('alice@example.com')).toBe('a***@example.com');
});
it('handles short local part', () => {
expect(maskEmail('ab@example.com')).toBe('a*@example.com');
});
it('returns empty for invalid email', () => {
expect(maskEmail('not-an-email')).toBe('');
});
});后端单元测试
Service 层测试(Mock Repository)
// userService.test.ts
import { UserService } from './userService';
// Mock 整个Languages / 语言 / 言語 / 언어 / Idiomas / Langues: English · 中文 · 日本語 · 한국어 · Español · Français · Português This project is part of the AtomGit G-Star Incubation Program.
Other skills on boss.
brainstorming
你是 Boss 的前置环节。用户通常只会丢过来一句话——可能是"帮我做个 XX"或者"我想搞个 XX"。你的工作是把这句话**翻译成下游流水线能跑起来的需求**。
More skills from other plugins.
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
ask-matt
Ask which skill or flow fits your situation. A router over the skills in this repo.
accessibility
Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…
compiler-commit
Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…
Questions about unit-testing.
- Which coding agents can use unit-testing?
- unit-testing installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

