# Javascript Testing Patterns

> 何时使用

- Skill: `findscripter/javascript-testing-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add findscripter/javascript-testing-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/findscripter/javascript-testing-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: findscripter (https://skillmd.com/u/findscripter)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/findscripter/javascript-testing-patterns

---

## 何时使用

适用场景：
- 为新项目搭建测试基础设施（Jest 或 Vitest）。
- 为纯函数、类、异步方法编写单元测试。
- 为 API、Service、数据库仓储编写集成测试。
- mock 外部依赖（fetch、第三方模块、SMTP 等）或用依赖注入解耦。
- 测试 React/Vue 等前端组件与自定义 Hook。
- 实践 TDD，在 CI/CD 中配置持续测试与覆盖率门槛。

不该用（负边界）：
- 任务与 JS/TS 测试无关，或涉及其他语言/工具栈（如 Python pytest、Go test）。
- 把测试输出当作环境特定验证、上线验证或专家评审的替代品——测试不等于生产可用证明。
- 缺少必要输入（被测代码、依赖边界、成功判据）时，应先澄清而非臆造。

## 步骤

1. 选框架：Vite 项目优先 Vitest（原生快、API 与 Jest 兼容）；通用 Node/TS 项目可用 Jest（`ts-jest` preset）。
2. 配置覆盖率门槛与 setup 文件（见示例 config）。
3. 按 AAA 模式（Arrange-Act-Assert）写测试，从纯函数到类、再到异步与集成逐层推进。
4. 隔离外部依赖：模块用 `vi.mock`，可控对象用依赖注入 + mock，观察调用用 `vi.spyOn`。
5. 用 fixture 工厂（`@faker-js/faker`）生成一致测试数据，避免硬编码散落。
6. 前端用 Testing Library：优先语义查询（getByRole/getByPlaceholderText），`data-testid` 少用。
7. 集成测试用 `supertest` 打真实路由，配 `beforeAll/afterAll` 建表清表、`beforeEach` 清数据。
8. 接入 CI：`vitest --coverage`，覆盖率目标 80%+。

## 指令

- 明确目标、约束与必需输入后再动手。
- 测试行为而非实现细节；覆盖错误分支与边界，而不仅是 happy path。
- 命名要描述「测什么」；一条测试一个逻辑断言。
- 用 `beforeEach/afterEach` 做建立与清理，防止测试间污染（mock 用 `vi.clearAllMocks()`，spy 用 `mockRestore()`）。
- 慢操作（网络、定时器）一律 mock；定时器用 `vi.useFakeTimers()` + `vi.advanceTimersByTime()`。

## 示例

Vitest 配置（覆盖率 + setup）：
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['**/*.d.ts', '**/*.config.ts', '**/dist/**'],
    },
    setupFiles: ['./src/test/setup.ts'],
  },
});
```

Jest 覆盖率门槛（核心约束）：
```typescript
// jest.config.ts
coverageThreshold: {
  global: { branches: 80, functions: 80, lines: 80, statements: 80 },
}
```

单元测试纯函数（含异常分支）：
```typescript
import { describe, it, expect } from 'vitest';
import { divide } from './calculator';

describe('divide', () => {
  it('should divide two numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });
  it('should throw when dividing by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });
});
```

异步 + mock fetch：
```typescript
global.fetch = vi.fn();

it('should fetch user successfully', async () => {
  const mockUser = { id: '1', name: 'John' };
  (fetch as any).mockResolvedValueOnce({ ok: true, json: async () => mockUser });

  const user = await service.fetchUser('1');
  expect(user).toEqual(mockUser);
  expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/1');
});

it('should throw if not ok', async () => {
  (fetch as any).mockResolvedValueOnce({ ok: false });
  await expect(service.fetchUser('999')).rejects.toThrow('User not found');
});
```

依赖注入 mock 仓储（比 mock 全局更可控）：
```typescript
beforeEach(() => {
  mockRepository = { findById: vi.fn(), create: vi.fn() };
  service = new UserService(mockRepository);
});

it('should throw if user not found', async () => {
  vi.mocked(mockRepository.findById).mockResolvedValue(null);
  await expect(service.getUser('999')).rejects.toThrow('User not found');
});
```

React 组件测试（Testing Library，语义查询优先）：
```typescript
import { render, screen, fireEvent } from '@testing-library/react';

it('should call onSubmit with form data', () => {
  const onSubmit = vi.fn();
  render(<UserForm onSubmit={onSubmit} />);
  fireEvent.change(screen.getByPlaceholderText('Name'), { target: { value: 'John' } });
  fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
  expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({ name: 'John' }));
});
```

Hook 测试（`renderHook` + `act`）：
```typescript
import { renderHook, act } from '@testing-library/react';

it('should increment count', () => {
  const { result } = renderHook(() => useCounter());
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);
});
```

API 集成测试（supertest + 数据库生命周期）：
```typescript
import request from 'supertest';

beforeEach(async () => { await pool.query('TRUNCATE TABLE users CASCADE'); });

it('should create a new user', async () => {
  const res = await request(app).post('/api/users').send(userData).expect(201);
  expect(res.body).not.toHaveProperty('password'); // 不泄露敏感字段
});
```

fixture 工厂（faker，数据一致）：
```typescript
import { faker } from '@faker-js/faker';

export function createUserFixture(overrides?: Partial<User>): User {
  return {
    id: faker.string.uuid(),
    name: faker.person.fullName(),
    email: faker.internet.email(),
    ...overrides,
  };
}
```

定时器测试：
```typescript
vi.useFakeTimers();
const cb = vi.fn();
setTimeout(cb, 1000);
expect(cb).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000);
expect(cb).toHaveBeenCalled();
vi.useRealTimers();
```

package.json 脚本：
```json
{ "scripts": {
  "test": "vitest",
  "test:coverage": "vitest --coverage",
  "test:ui": "vitest --ui"
}}
```

## 注意事项

- 一条测试一个逻辑断言；测行为不测实现，避免组件重构即红的脆弱测试。
- 必清理：mock 用 `vi.clearAllMocks()`，spy 用 `mockRestore()`，集成用 `afterAll` 关连接（`pool.end()`），否则测试相互污染、句柄泄漏。
- 异步必须 `await`：断言 reject 用 `await expect(...).rejects.toThrow(...)`，漏 await 会假绿。
- 快照测试谨慎：大快照难审、易盲目 `--update`；仅用于稳定、小粒度的 UI 输出。
- `data-testid` 少用，优先语义查询，更贴近真实用户行为。
- 覆盖率是下限不是目标——80% 覆盖不等于测试有效，关键是覆盖错误分支与边界。
- 集成测试依赖真实数据库/服务，需独立测试库与隔离环境，勿连生产或共享开发库。

## 互见

- 前端 E2E、组件交互可配合 Chrome DevTools/Lighthouse 类工具做端到端与性能验证。
- CI/CD 中将 `test:coverage` 作为合并门禁的一环。
- 参考文档：Vitest（vitest.dev）、Jest（jestjs.io）、Testing Library（testing-library.com）。

---
采编自 sickn33/antigravity-awesome-skills（MIT 许可），已按「技能大典」SCHEMA 适配重写。

