测试基础

张玥 2026年2月14日 阅读时间 40分钟
单元测试 Jest Mocha TDD BDD
JavaScript测试基础

测试是保障代码质量、降低维护成本的关键环节。JavaScript 生态提供了丰富的测试工具和框架,从单元测试到端到端测试,覆盖开发全流程。本文将带你系统学习 JavaScript 测试基础,包括测试类型、主流框架 Jest 的使用、异步测试、Mock、代码覆盖率以及最佳实践。通过 40 分钟的深入学习,你将能编写可靠、可维护的测试用例,为项目健壮性打下坚实基础。

测试基础概念

软件测试是在规定条件下检查程序功能的过程。在前端/Node.js 领域,主要包含以下层级:

// 测试类型金字塔
// 1. 单元测试:验证独立函数/模块
: test('adds 1 + 2 to equal 3', () => {
  expect(sum(1,2)).toBe(3);
});

// 2. 集成测试:测试模块间交互
: test('API 请求返回正确数据', async () => {
  const data = await fetchUser(1);
  expect(data.name).toBe('Alice');
});

// 3. E2E 测试:模拟真实用户场景
: cy.visit('/login'); cy.get('#username').type('test');
测试金字塔
单元测试 (70%)
集成测试 (20%)
E2E (10%)

越底层测试越轻量、运行越快

Jest 快速入门

Jest 是目前最流行的零配置测试框架,内置断言、Mock、覆盖率收集。

// 安装
npm install --save-dev jest

// package.json 添加脚本
"scripts": { "test": "jest" }

// 第一个测试 (sum.js)
function sum(a,b) { return a+b; }
module.exports = sum;

// sum.test.js
const sum = require('./sum');
test('1 + 2 = 3', () => {
  expect(sum(1,2)).toBe(3);
});
PASS sum.test.js
✓ 1 + 2 = 3 2ms
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total

常用匹配器 (Matchers)

Jest 提供丰富的匹配器让断言更具表达力。

// 精确相等
expect(value).toBe(42);
expect({name:'Alice'}).toEqual({name:'Alice'});

// 真值判断
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).toBeTruthy();

// 数字比较
expect(2.3).toBeGreaterThan(2);
expect(0.1+0.2).toBeCloseTo(0.3);

// 字符串/正则
expect('team').toMatch(/tea/);

// 数组/可迭代对象
expect([1,2,3]).toContain(2);
toBe toEqual toMatch toContain toThrow

sum.test.js 中尝试各种匹配器

测试异步代码

JavaScript 大量异步操作,Jest 支持回调、Promise 和 async/await。

// 回调方式 (done)
test('fetch data', done => {
  fetchData((data) => {
    expect(data).toBe('peanut butter');
    done();
  });
});

// Promise 方式
test('resolves to lemon', () => {
  return expect(Promise.resolve('lemon')).resolves.toBe('lemon');
});

// async/await
test('with async/await', async () => {
  const data = await fetchDataAsync();
  expect(data).toBe('apple');
});
PASS async.test.js
✓ resolves to lemon (5ms)
✓ with async/await (3ms)
异步测试通过

钩子函数 (Hooks)

beforeEach / afterEach / beforeAll / afterAll 用来设置测试前置/后置条件。

let cityDB;

beforeAll(() => {
  cityDB = connectDatabase();
});

afterAll(() => {
  cityDB.close();
});

beforeEach(() => {
  cityDB.clear();
  cityDB.add('北京');
});

test('has city Beijing', () => {
  expect(cityDB.has('北京')).toBeTruthy();
});
beforeAll → 执行一次
beforeEach → 每个测试前
afterEach / afterAll 清理

Mock 函数与模拟

使用 jest.fn() 模拟函数行为,隔离外部依赖。

const mockCallback = jest.fn(x => 42 + x);
[1,2].forEach(mockCallback);

expect(mockCallback.mock.calls.length).toBe(2);
expect(mockCallback.mock.calls[0][0]).toBe(1);
expect(mockCallback.mock.results[0].value).toBe(43);

// 模拟模块 (axios)
jest.mock('axios');
axios.get.mockResolvedValue({ data: 'ok' });
mockFn.mock.calls
mockFn.mock.results
mockFn.mock.instances

检查调用次数、参数和返回值

测试 UI 组件 (Testing Library)

结合 @testing-library/react 测试组件行为而非实现。

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';

test('点击按钮增加计数', async () => {
  render();
  const button = screen.getByRole('button', {name: /increment/i});
  await userEvent.click(button);
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
Count: 1

通过用户事件触发,验证 DOM 变化

代码覆盖率

Jest 内置覆盖率收集,通过 --coverage 生成报告。

// 运行测试并收集覆盖率
npm test -- --coverage

// 输出示例
-----------|---------|----------|---------|
File | % Stmts | % Branch | % Funcs |
-----------|---------|----------|---------|
math.js | 100 | 75 | 100 |
85%
语句: 85%
分支: 72%
函数: 91%

帮助发现未测试的代码路径

测试最佳实践

  • 单一断言:每个 test 尽量只验证一个行为
  • 描述清晰:test('should ...') 使用自然语言
  • 避免测试实现细节:测试公共接口而非内部逻辑
  • 模拟外部服务:使用 mock 避免真实网络请求
  • 持续集成:在 CI 中自动运行测试
  • 尊重用户偏好:对动画或定时器使用 jest.useFakeTimers()
// 使用 fake timers 控制时间
jest.useFakeTimers();
timerGame();
expect(setTimeout).toHaveBeenCalledTimes(1);