测试
测试是保证代码质量的重要手段。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');
});
小结
本章我们学习了:
- Node.js 内置测试:
node:test模块 - Jest:流行测试框架、匹配器、Mock
- Mocha + Chai:灵活的测试组合
- Supertest:API 测试
- 测试覆盖率:c8、Jest coverage
- 最佳实践:组织、命名、AAA 模式
练习
- 使用 Node.js 内置测试模块编写单元测试
- 使用 Jest 编写一个 Express API 的测试
- 实现测试覆盖率报告
- 使用 Mock 测试依赖外部服务的代码