首页 / Node.js 教程 / 测试策略与 Jest

Node.js 教程

测试策略与 Jest

本教程共 76 篇 · 第 61 篇 · 更新于 2026-07-25 · 约 8 分钟阅读

Node.js测试JestTDD单元测试

61. 测试策略与 Jest

本节目标:- 测试金字塔:单测、集成、E2E 各自的职责与边界 - TDD(测试驱动开发)的基本节奏 - Jest / Mocha / Vitest / node:test 怎么选 - Jest 实战:安装、describe/it/expect、mock、覆盖率、watch

上一章我们用了 Node.js 内置的 node:test。它够轻、零依赖,但真到了大项目,你可能会想要更「全家桶」的东西:开箱即用的断言库、更顺手的 mock、快照测试、覆盖率一键生成。这就是 Jest 这类框架存在的理由。

不过,工具只是手段。比「用哪个框架」更重要的是「测什么、怎么分层」。这一章我们先聊测试策略——单元测试、集成测试、端到端测试到底差在哪,再横向对比几个主流框架,最后上手 Jest。

测试金字塔

测试不是越多越好,也不是越细越好。业界有个经典模型叫「测试金字塔」:底层是大量的单元测试,中间是较少的集成测试,顶层是更少的端到端(E2E)测试。越往上越慢、越脆弱、越贵。

单元测试

只测最小的一块逻辑——一个函数、一个方法,把它依赖的东西都 mock 掉,只看自己这块对不对。上一章的 adddivide 测试就是典型单测。它跑得飞快,一个项目几千条单测也能在几秒内跑完。

// 单测:只验证函数本身,数据库/网络都换成替身
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

function isAdult(user) {
  return user.age >= 18;
}

describe('isAdult', () => {
  it('成年人返回 true', () => {
    assert.equal(isAdult({ age: 23 }), true);
  });
  it('未成年返回 false', () => {
    assert.equal(isAdult({ age: 12 }), false);
  });
});

集成测试

把几个组件真正拼起来跑,比如「路由 + 控制器 + 真实(或测试用)数据库」。它验证的是「接缝处」,也就是模块之间是不是真能协同工作。单测全绿但集成挂掉,多半是接口对不上。

下面这个例子起一个真实 HTTP 服务,再发请求验证响应:

import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import http from 'node:http';
import app from './app.js'; // 导出的 Express 应用

describe('GET /users', () => {
  it('返回两个用户', async () => {
    const server = http.createServer(app);
    await new Promise(resolve => server.listen(0, resolve));
    const port = server.address().port;

    try {
      const res = await fetch(`http://localhost:${port}/users`);
      assert.equal(res.status, 200);
      const users = await res.json();
      assert.equal(users.length, 2);
    } finally {
      await new Promise(resolve => server.close(resolve));
    }
  });
});

注意 finally 里一定要把服务关掉,否则测试进程不会退出,会卡住 CI。我踩过这个坑,排查半天以为测试挂了,其实是端口没释放。

端到端测试(E2E)

把整个应用当黑盒,用真实浏览器或真实客户端去操作,连数据库、缓存、第三方服务都是真的。这类测试最贴近用户真实体验,但也最慢、最易碎。常用 Playwright、Cypress 这类工具驱动浏览器点击操作。一般只挑核心流程(登录、下单、支付)来覆盖,不求全。

Note

一个经验法则:E2E 里不要 mock。E2E 的价值就在于「全链路真实」,一旦把关键依赖换成假货,测的就不是真实行为了。

TDD:先写测试再写实现

测试驱动开发(Test-Driven Development,TDD)是一种工作流:先写会失败的测试,再写刚好让测试通过的最小实现,最后重构。循环是「红 → 绿 → 重构」。

// 1. 先写测试(此时 validatePassword 还不存在,必然失败)
import assert from 'node:assert/strict';
assert.equal(validatePassword('abc12'), false);   // 太短
assert.equal(validatePassword('abcdef123'), true); // 够长且有数字

// 2. 写最小实现让它通过
function validatePassword(pw) {
  if (pw.length < 8) return false;
  if (!/\d/.test(pw)) return false;
  return true;
}

// 3. 重构、加新需求,重复循环

TDD 不是银弹,但它有个实在的好处:逼你想清楚「这个函数到底要被怎么用」才会动手写。对 API 设计和边界条件考虑得更周全。

框架怎么选

市面主流就这几个,各有地盘:

框架是否内置零配置内置 mock覆盖率适合场景
node:test✅ 内置基础需 V8 工具轻量项目、不想加依赖
Jest✅ 强✅ 内置全家桶、前后端通吃
Mocha❌ 需 Sinon需 nyc灵活、插件多
Vitest✅ 内置已用 Vite、要快、TS 好

我的建议:小工具、库、想保持零依赖,直接用 node:test;业务项目、团队协作、要快照和丰富 mock,上 Jest;如果项目已经基于 Vite 构建,Vitest 几乎零成本接入,且速度更快。

Tip

别为了「看起来专业」硬上重型框架。我见过一个几百行的小脚本配了全套 Jest + 覆盖率阈值,维护成本比代码本身还高。

Jest 上手

Jest 是 Facebook 出品,主打「开箱即用」。先装:

npm install --save-dev jest

Jest 默认认 *.test.js__tests__/ 目录,基本不用配置就能跑。写法和 node:test 很像,但断言换成了 expect(...).toBe(...) 这种「期望式」语法:

// utils/math.js
export function sum(a, b) {
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new Error('Both arguments must be numbers');
  }
  return a + b;
}

export function divide(a, b) {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
}
// __tests__/math.test.js
import { sum, divide } from '../utils/math.js';

describe('Math 工具', () => {
  describe('sum()', () => {
    it('正确相加', () => {
      expect(sum(1, 2)).toBe(3);
      expect(sum(-1, 1)).toBe(0);
    });

    it('非数字抛错', () => {
      expect(() => sum('1', 2)).toThrow('Both arguments must be numbers');
    });
  });

  describe('divide()', () => {
    it('正确相除', () => {
      expect(divide(10, 2)).toBe(5);
    });

    it('除零抛错', () => {
      expect(() => divide(10, 0)).toThrow('Division by zero');
    });
  });
});

expect 的 matcher 很丰富:toBe 严格相等、toEqual 深比较、toThrow 测异常、toMatch 正则匹配等等。报错信息也比 assert 友好,会直接把期望值和实际值并排打出来。

运行与常用开关

# 跑全部
npx jest

# 监听模式,改文件自动重跑
npx jest --watch

# 只跑名字匹配的用例
npx jest -t "sum"

# 生成覆盖率报告(开箱即用)
npx jest --coverage

Jest 的 --coverage 不用装任何额外工具,直接给你行级、分支、函数、语句四个维度的覆盖率,还会在终端画出表格。这是它相对 node:test 最省心的地方。

Jest 的 mock

Jest 自带一套完整的 mock 机制,比 node:test 的更顺手。

// 造一个函数替身
const mockFn = jest.fn();
mockFn('hello');
expect(mockFn).toHaveBeenCalledWith('hello');

// mock 一个模块
jest.mock('axios');
import axios from 'axios';
axios.get.mockResolvedValue({ data: { ok: true } });

jest.fn() 记录调用,jest.mock() 把整个模块换掉。对于「不想真发网络请求」的场景,这套组合拳用起来比手动写替身舒服。

快照测试

Jest 带火了快照测试(Snapshot):第一次运行时把输出存成快照文件,之后每次运行都和快照比对。UI 渲染、配置文件输出这类「结构稳定但手写断言麻烦」的东西特别适合:

it('渲染结构稳定', () => {
  const tree = renderUserProfile(user);
  expect(tree).toMatchSnapshot();
});
Warning

快照不是免死金牌。它容易「脆性」——你只是改了个无关紧要的文案,快照就报错,于是有人习惯性地 jest -u 一键更新,等于变相关掉了测试。快照要当作契约来对待,更新前先想清楚为什么变了。

把策略落到项目里

一个务实的做法:

  • 单测占大头,覆盖纯逻辑、工具函数、边界与异常分支,跑得快,提交前本地就跑。
  • 集成挑关键路径,比如「注册接口真的写进了数据库」。
  • E2E 只覆盖最值钱的用户旅程,放 CI 里夜里慢慢跑。

框架层面,新项目从 node:test 起步完全够用,等你觉得想要快照、想要更丝滑的 mock、想要零配置覆盖率时,再迁移到 Jest 也不迟——两者的用例写法高度相似,迁移成本主要在断言语法上。