Jest 是由 Facebook 开发的零配置测试框架,广泛应用于 JavaScript 项目的单元测试。它集成了断言库、Mock 功能、覆盖率报告等,使得测试变得简单高效。本章将带你从零开始学习如何在 Node.js 中使用 Jest 编写和运行单元测试。
单元测试是对软件中的最小可测试单元(如函数、模块)进行检查和验证。它的好处包括:
首先创建一个 Node.js 项目并安装 Jest:
mkdir jest-demo
cd jest-demo
npm init -y
npm install --save-dev jest
然后在 package.json 中添加测试脚本:
"scripts": {
"test": "jest"
}
现在可以运行 npm test 执行测试(默认会查找 *.test.js 或 *.spec.js 文件)。
创建一个简单的函数模块 math.js:
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
编写测试文件 math.test.js:
// math.test.js
const { add, subtract } = require('./math');
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
test('subtracts 5 - 2 to equal 3', () => {
expect(subtract(5, 2)).toBe(3);
});
运行 npm test,Jest 会执行测试并显示结果。
test(name, fn, timeout):定义一个测试用例。describe(name, fn):将多个相关测试分组。expect(value):生成断言对象。示例:
describe('数学函数', () => {
test('加法', () => {
expect(add(1, 2)).toBe(3);
});
test('减法', () => {
expect(subtract(5, 2)).toBe(3);
});
});
Jest 提供了丰富的匹配器来断言不同情况:
.toBe(value):严格相等(===)。.toEqual(value):递归检查对象或数组的每个字段。.toBeTruthy() / .toBeFalsy().toContain(item):数组或可迭代对象包含。.toThrow(error):函数执行是否抛出异常。test('对象相等', () => {
const data = { one: 1 };
data['two'] = 2;
expect(data).toEqual({ one: 1, two: 2 });
});
test('数组包含', () => {
expect(['apple', 'banana']).toContain('apple');
});
Node.js 中有大量异步操作,Jest 支持多种方式测试异步代码。
使用 done 参数,当异步完成时调用 done()。
test('异步回调', (done) => {
function callback(data) {
try {
expect(data).toBe('hello');
done();
} catch (err) {
done(err);
}
}
fetchData(callback);
});
直接返回 Promise,Jest 会等待其解决。
test('promise 解决', () => {
return fetchData().then(data => {
expect(data).toBe('hello');
});
});
使用 async/await 使测试更简洁。
test('async/await', async () => {
const data = await fetchData();
expect(data).toBe('hello');
});
Jest 提供钩子在测试前后执行准备和清理工作:
beforeAll(fn, timeout):在所有测试开始前执行一次。afterAll(fn):在所有测试结束后执行一次。beforeEach(fn):在每个测试开始前执行。afterEach(fn):在每个测试结束后执行。常用于初始化数据库连接、创建测试数据等。
let db;
beforeAll(() => {
db = connectDatabase();
});
afterAll(() => {
db.close();
});
test('查询用户', () => {
const user = db.findUser(1);
expect(user.name).toBe('Alice');
});
Mock 函数可以模拟外部依赖,控制其行为并记录调用信息。Jest 使用 jest.fn() 创建模拟函数。
const callback = jest.fn();
test('mock 函数', () => {
callback('hello');
expect(callback).toHaveBeenCalled();
expect(callback).toHaveBeenCalledWith('hello');
expect(callback).toHaveBeenCalledTimes(1);
});
const mockFn = jest.fn();
mockFn.mockReturnValue(42);
console.log(mockFn()); // 42
使用 jest.mock() 自动模拟整个模块。例如模拟 axios:
const axios = require('axios');
jest.mock('axios');
axios.get.mockResolvedValue({ data: { id: 1 } });
test('获取数据', async () => {
const result = await fetchUser();
expect(axios.get).toHaveBeenCalledWith('/user/1');
expect(result).toEqual({ id: 1 });
});
Jest 内置了测试覆盖率报告,通过 --coverage 标志生成。在 package.json 中配置:
"scripts": {
"test": "jest --coverage"
}
运行后会显示未覆盖的代码行、分支、函数等信息,并生成 HTML 报告(coverage/ 目录)。
对于 Node.js 特有的模块(如 fs),也可以使用 Jest 模拟。例如测试一个文件读取函数:
const fs = require('fs');
jest.mock('fs');
const { readConfig } = require('./config');
test('读取配置文件', () => {
fs.readFileSync.mockReturnValue('{"name":"test"}');
const config = readConfig('config.json');
expect(config).toEqual({ name: 'test' });
expect(fs.readFileSync).toHaveBeenCalledWith('config.json', 'utf8');
});
在真实的项目中,通常会将测试文件放在 __tests__ 目录下,或与源文件放在一起(如 math.js 旁边放 math.test.js)。Jest 会自动识别。
可以通过 jest.config.js 自定义 Jest 行为:
module.exports = {
testEnvironment: 'node', // 环境 (node 或 jsdom)
collectCoverage: true,
coverageDirectory: 'coverage',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)']
};
假设有一个用户服务,依赖数据库和邮件服务:
// userService.js
const db = require('./db');
const email = require('./email');
async function createUser(name, emailAddress) {
const user = { name, email: emailAddress };
await db.save(user);
await email.sendWelcome(emailAddress);
return user;
}
module.exports = { createUser };
测试:
jest.mock('./db');
jest.mock('./email');
const { createUser } = require('./userService');
const db = require('./db');
const email = require('./email');
test('创建用户', async () => {
db.save.mockResolvedValue(true);
email.sendWelcome.mockResolvedValue(true);
const user = await createUser('Alice', 'alice@example.com');
expect(db.save).toHaveBeenCalledWith({ name: 'Alice', email: 'alice@example.com' });
expect(email.sendWelcome).toHaveBeenCalledWith('alice@example.com');
expect(user).toEqual({ name: 'Alice', email: 'alice@example.com' });
});
test('name', fn, 10000)。jest.restoreAllMocks() 或 jest.clearAllMocks() 在钩子中清理。rewire 或 babel-plugin-rewire 访问私有函数。Jest 提供了零配置、功能强大的测试环境,非常适合 Node.js 项目的单元测试。通过本章学习,你应该掌握了 Jest 的基本用法、异步测试、Mock 和覆盖率报告。结合 TDD(测试驱动开发)理念,可以显著提升代码质量和可维护性。下一章我们将学习集成测试与端到端测试。