跳到主要内容

测试

测试是保证代码质量的重要手段。Node.js 提供了内置的测试模块,同时也支持各种第三方测试框架。

Node.js 内置测试

Node.js 20+ 提供了内置的测试模块:

基本使用

// math.test.js
const { test, describe, it, assert } = require('node:test');

describe('数学运算', () => {
test('加法', () => {
assert.strictEqual(1 + 1, 2);
});

test('减法', () => {
assert.strictEqual(5 - 3, 2);
});

it('乘法', () => {
assert.strictEqual(2 * 3, 6);
});
});

运行测试:

node --test
node --test math.test.js

断言方法

const { test, assert } = require('node:test');

test('断言示例', () => {
// 相等断言
assert.strictEqual(1, 1); // 严格相等 ===
assert.notStrictEqual(1, '1'); // 不相等 !==
assert.deepEqual([1, 2], [1, 2]); // 深度相等
assert.notDeepEqual([1], [2]); // 深度不等

// 布尔断言
assert.ok(true); // 为真
assert.ifError(null); // 为假值

// 错误断言
assert.throws(() => { throw new Error(); });
assert.doesNotThrow(() => {});

// 异常断言
assert.rejects(async () => { throw new Error(); });
assert.doesNotReject(async () => {});
});

异步测试

const { test, assert } = require('node:test');

// Promise
test('Promise 测试', async () => {
const result = await Promise.resolve(42);
assert.strictEqual(result, 42);
});

// 回调
test('回调测试', (t, done) => {
setTimeout(() => {
assert.ok(true);
done();
}, 100);
});

测试钩子

const { test, describe, beforeEach, afterEach } = require('node:test');

describe('带钩子的测试', () => {
let data;

beforeEach(() => {
data = { count: 0 };
});

afterEach(() => {
data = null;
});

test('测试1', () => {
data.count++;
assert.strictEqual(data.count, 1);
});

test('测试2', () => {
assert.strictEqual(data.count, 0); // 每个 test 前都会重置
});
});

跳过和只运行

const { test, describe, it } = require('node:test');

// 跳过测试
test.skip('跳过的测试', () => {
// 不会执行
});

// 只运行特定测试
test.only('只运行这个测试', () => {
// 只运行标记 only 的测试
});

// 条件跳过
test('条件测试', { skip: process.platform === 'win32' }, () => {
// Windows 上跳过
});

// 标记为待完成
test.todo('待完成的测试');

Jest

Jest 是最流行的 JavaScript 测试框架之一。

安装

npm install --save-dev jest

基本使用

// math.test.js
describe('数学运算', () => {
test('加法', () => {
expect(1 + 1).toBe(2);
});

test('对象比较', () => {
expect({ a: 1 }).toEqual({ a: 1 });
});
});

匹配器

test('匹配器示例', () => {
// 相等
expect(1).toBe(1);
expect({ a: 1 }).toEqual({ a: 1 });
expect({ a: undefined }).toStrictEqual({ a: undefined });

// 真值
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(1).toBeDefined();

// 数字
expect(1).toBeGreaterThan(0);
expect(1).toBeLessThan(2);
expect(0.1 + 0.2).toBeCloseTo(0.3);

// 字符串
expect('hello world').toMatch(/hello/);
expect('hello').toContain('ell');

// 数组
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);

// 异常
expect(() => { throw new Error('err'); }).toThrow();
expect(() => { throw new Error('err'); }).toThrow('err');

// 异步
await expect(Promise.resolve(1)).resolves.toBe(1);
await expect(Promise.reject('err')).rejects.toBe('err');
});

异步测试

// Promise
test('Promise', async () => {
const data = await fetchData();
expect(data).toBeDefined();
});

// 回调
test('回调', done => {
callback(data => {
expect(data).toBe('ok');
done();
});
});

Mock

// 函数 Mock
test('函数 Mock', () => {
const mockFn = jest.fn();
mockFn('hello');

expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith('hello');
expect(mockFn).toHaveBeenCalledTimes(1);

// 设置返回值
mockFn.mockReturnValue('default');
expect(mockFn()).toBe('default');

// 设置实现
mockFn.mockImplementation(x => x * 2);
expect(mockFn(2)).toBe(4);
});

// 模块 Mock
jest.mock('./api');
const api = require('./api');

test('模块 Mock', async () => {
api.getUser.mockResolvedValue({ id: 1, name: 'Alice' });

const user = await api.getUser(1);
expect(user.name).toBe('Alice');
});

钩子

beforeAll(() => {
// 所有测试前执行一次
});

afterAll(() => {
// 所有测试后执行一次
});

beforeEach(() => {
// 每个测试前执行
});

afterEach(() => {
// 每个测试后执行
});

Mocha + Chai

Mocha 是灵活的测试框架,Chai 是断言库。

安装

npm install --save-dev mocha chai

基本使用

const { expect } = require('chai');

describe('数学运算', function() {
it('加法', function() {
expect(1 + 1).to.equal(2);
});

it('减法', function() {
expect(5 - 3).to.equal(2);
});
});

断言风格

const chai = require('chai');
const expect = chai.expect;
const assert = chai.assert;
const should = chai.should();

// expect 风格
expect(1).to.equal(1);
expect([1, 2]).to.have.lengthOf(2);

// assert 风格
assert.equal(1, 1);
assert.isArray([1, 2]);

// should 风格
[1, 2].should.have.lengthOf(2);

异步测试

describe('异步测试', function() {
it('Promise', async function() {
const result = await fetchData();
expect(result).to.exist;
});

it('回调', function(done) {
asyncFunction(data => {
expect(data).to.equal('ok');
done();
});
});
});

Supertest (API 测试)

安装

npm install --save-dev supertest

Express API 测试

const request = require('supertest');
const express = require('express');
const app = express();

app.get('/users', (req, res) => {
res.json([{ id: 1, name: 'Alice' }]);
});

app.post('/users', express.json(), (req, res) => {
res.status(201).json({ id: 2, ...req.body });
});

describe('用户 API', () => {
test('GET /users', async () => {
const res = await request(app).get('/users');

expect(res.status).toBe(200);
expect(res.body).toHaveLength(1);
expect(res.body[0].name).toBe('Alice');
});

test('POST /users', async () => {
const res = await request(app)
.post('/users')
.send({ name: 'Bob', email: '[email protected]' });

expect(res.status).toBe(201);
expect(res.body.name).toBe('Bob');
});
});

测试覆盖率

使用 c8

npm install --save-dev c8

# 运行测试并生成覆盖率报告
npx c8 node --test

# 生成 HTML 报告
npx c8 --reporter=html node --test

Jest 覆盖率

# 运行覆盖率
npx jest --coverage
// jest.config.js
module.exports = {
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};

测试最佳实践

组织测试文件

project/
├── src/
│ ├── utils/
│ │ ├── math.js
│ │ └── math.test.js
│ └── api/
│ ├── users.js
│ └── users.test.js
└── tests/
├── setup.js
└── integration/
└── api.test.js

测试命名

// 好的测试命名
describe('UserService', () => {
describe('createUser', () => {
it('should create a new user with valid data', () => {});
it('should throw error when email is invalid', () => {});
it('should throw error when user already exists', () => {});
});
});

测试结构 (AAA 模式)

test('用户创建', async () => {
// Arrange - 准备
const userData = { name: 'Alice', email: '[email protected]' };

// Act - 执行
const user = await createUser(userData);

// Assert - 断言
expect(user.id).toBeDefined();
expect(user.name).toBe('Alice');
});

小结

本章我们学习了:

  1. Node.js 内置测试node:test 模块
  2. Jest:流行测试框架、匹配器、Mock
  3. Mocha + Chai:灵活的测试组合
  4. Supertest:API 测试
  5. 测试覆盖率:c8、Jest coverage
  6. 最佳实践:组织、命名、AAA 模式

练习

  1. 使用 Node.js 内置测试模块编写单元测试
  2. 使用 Jest 编写一个 Express API 的测试
  3. 实现测试覆盖率报告
  4. 使用 Mock 测试依赖外部服务的代码