Skip to content
Development
Skill

/unit-testing

单元测试方法论,覆盖 AAA 模式、Test Doubles、跨语言示例(JS/TS、Python、Go、Java)、前后端单元测试、边界与属性测试、变异测试

From plugin
boss
54438 skills16 agents10 commands

Claude Code

$ npx -y skills add echoVic/boss-skill --skill unit-testing --agent claude-code

SKILL.md

unit-testing.SKILL.md
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 整个
Read more
@echovic
Ships withboss

Languages / 语言 / 言語 / 언어 / Idiomas / Langues: English · 中文 · 日本語 · 한국어 · Español · Français · Português This project is part of the AtomGit G-Star Incubation Program.

54438 skills16 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
544
Star history of echoVic/boss-skill on GitHub, a chart by star-history.com

Other skills on boss.

brainstorming
Skill

brainstorming

你是 Boss 的前置环节。用户通常只会丢过来一句话——可能是"帮我做个 XX"或者"我想搞个 XX"。你的工作是把这句话**翻译成下游流水线能跑起来的需求**。

@echovic@echovicInstall Skill

More skills from other plugins.

brainstorming
Skill
Popular
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
accessibility
Skill
Popular
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc1d293k
@affaan-m@affaan-mInstall Skill
compiler-commit
Skill
Popular

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react1d252k
@facebook@facebookInstall Skill

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.