Jest Node.js 测试 #
Node.js 测试概述 #
Node.js 后端测试是确保 API 可靠性的重要手段,包括单元测试、集成测试和 API 测试。
text
┌─────────────────────────────────────────────────────────────┐
│ Node.js 测试内容 │
├─────────────────────────────────────────────────────────────┤
│ 1. 单元测试 - 测试独立函数和模块 │
│ 2. API 测试 - 测试 API 端点 │
│ 3. 数据库测试 - 测试数据库操作 │
│ 4. 中间件测试 - 测试 Express/Koa 中间件 │
│ 5. 集成测试 - 测试多个模块协作 │
│ 6. Mock 外部服务 - 模拟第三方服务 │
└─────────────────────────────────────────────────────────────┘
环境配置 #
安装依赖 #
bash
npm install --save-dev jest supertest
配置 Jest #
javascript
// jest.config.js
module.exports = {
testEnvironment: 'node',
testMatch: ['**/*.test.js'],
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js',
],
};
Express 测试 #
基本应用测试 #
javascript
// app.js
const express = require('express');
const app = express();
app.use(express.json());
app.get('/api/users', (req, res) => {
res.json([{ id: 1, name: 'John' }]);
});
app.post('/api/users', (req, res) => {
const user = { id: Date.now(), ...req.body };
res.status(201).json(user);
});
module.exports = app;
javascript
// app.test.js
const request = require('supertest');
const app = require('./app');
describe('User API', () => {
test('GET /api/users', async () => {
const response = await request(app).get('/api/users');
expect(response.status).toBe(200);
expect(response.body).toHaveLength(1);
expect(response.body[0]).toHaveProperty('name', 'John');
});
test('POST /api/users', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Jane' });
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('name', 'Jane');
expect(response.body).toHaveProperty('id');
});
});
带数据库的应用 #
javascript
// app.js
const express = require('express');
const { User } = require('./models');
const app = express();
app.use(express.json());
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
app.post('/api/users', async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
});
module.exports = app;
javascript
// app.test.js
const request = require('supertest');
const app = require('./app');
const { User } = require('./models');
jest.mock('./models');
describe('User API', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('GET /api/users/:id - success', async () => {
User.findById.mockResolvedValue({ id: 1, name: 'John' });
const response = await request(app).get('/api/users/1');
expect(response.status).toBe(200);
expect(response.body).toEqual({ id: 1, name: 'John' });
});
test('GET /api/users/:id - not found', async () => {
User.findById.mockResolvedValue(null);
const response = await request(app).get('/api/users/999');
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'User not found' });
});
test('POST /api/users', async () => {
User.create.mockResolvedValue({ id: 1, name: 'Jane' });
const response = await request(app)
.post('/api/users')
.send({ name: 'Jane' });
expect(response.status).toBe(201);
expect(response.body).toEqual({ id: 1, name: 'Jane' });
});
});
中间件测试 #
javascript
// authMiddleware.js
function authMiddleware(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
}
module.exports = authMiddleware;
javascript
// authMiddleware.test.js
const authMiddleware = require('./authMiddleware');
const { verifyToken } = require('./jwt');
jest.mock('./jwt');
describe('authMiddleware', () => {
let req, res, next;
beforeEach(() => {
req = { headers: {} };
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
next = jest.fn();
});
test('passes with valid token', () => {
req.headers.authorization = 'valid-token';
verifyToken.mockReturnValue({ id: 1 });
authMiddleware(req, res, next);
expect(next).toHaveBeenCalled();
expect(req.user).toEqual({ id: 1 });
});
test('fails without token', () => {
authMiddleware(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ error: 'No token provided' });
expect(next).not.toHaveBeenCalled();
});
test('fails with invalid token', () => {
req.headers.authorization = 'invalid-token';
verifyToken.mockImplementation(() => {
throw new Error('Invalid token');
});
authMiddleware(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ error: 'Invalid token' });
});
});
数据库测试 #
使用测试数据库 #
javascript
// database.test.js
const mongoose = require('mongoose');
const User = require('./models/User');
describe('Database Tests', () => {
beforeAll(async () => {
await mongoose.connect('mongodb://localhost:27017/test_db');
});
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
beforeEach(async () => {
await User.deleteMany({});
});
test('creates user', async () => {
const user = await User.create({
name: 'John',
email: 'john@example.com',
});
expect(user.id).toBeDefined();
expect(user.name).toBe('John');
});
test('finds user', async () => {
await User.create({ name: 'John', email: 'john@example.com' });
const user = await User.findOne({ name: 'John' });
expect(user).toBeDefined();
expect(user.email).toBe('john@example.com');
});
test('updates user', async () => {
const user = await User.create({ name: 'John', email: 'john@example.com' });
user.name = 'Jane';
await user.save();
const updated = await User.findById(user.id);
expect(updated.name).toBe('Jane');
});
test('deletes user', async () => {
const user = await User.create({ name: 'John', email: 'john@example.com' });
await User.findByIdAndDelete(user.id);
const deleted = await User.findById(user.id);
expect(deleted).toBeNull();
});
});
使用内存数据库 #
javascript
// mongodb-memory-server.test.js
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
服务层测试 #
javascript
// userService.js
class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}
async getUser(id) {
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error('User not found');
}
return user;
}
async createUser(data) {
if (!data.email) {
throw new Error('Email is required');
}
return this.userRepository.create(data);
}
}
module.exports = UserService;
javascript
// userService.test.js
const UserService = require('./userService');
describe('UserService', () => {
let userService;
let mockRepository;
beforeEach(() => {
mockRepository = {
findById: jest.fn(),
create: jest.fn(),
};
userService = new UserService(mockRepository);
});
test('getUser - success', async () => {
mockRepository.findById.mockResolvedValue({ id: 1, name: 'John' });
const user = await userService.getUser(1);
expect(user).toEqual({ id: 1, name: 'John' });
});
test('getUser - not found', async () => {
mockRepository.findById.mockResolvedValue(null);
await expect(userService.getUser(999)).rejects.toThrow('User not found');
});
test('createUser - success', async () => {
mockRepository.create.mockResolvedValue({ id: 1, name: 'John', email: 'john@example.com' });
const user = await userService.createUser({
name: 'John',
email: 'john@example.com',
});
expect(user).toHaveProperty('id');
});
test('createUser - missing email', async () => {
await expect(userService.createUser({ name: 'John' })).rejects.toThrow('Email is required');
});
});
外部服务 Mock #
Mock HTTP 请求 #
javascript
// externalApi.js
const axios = require('axios');
async function fetchExternalUser(id) {
const response = await axios.get(`https://api.example.com/users/${id}`);
return response.data;
}
module.exports = { fetchExternalUser };
javascript
// externalApi.test.js
const axios = require('axios');
const { fetchExternalUser } = require('./externalApi');
jest.mock('axios');
test('fetchExternalUser', async () => {
axios.get.mockResolvedValue({
data: { id: 1, name: 'John' },
});
const user = await fetchExternalUser(1);
expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users/1');
expect(user).toEqual({ id: 1, name: 'John' });
});
Mock 文件系统 #
javascript
// fileService.js
const fs = require('fs').promises;
async function readConfig(path) {
const content = await fs.readFile(path, 'utf-8');
return JSON.parse(content);
}
module.exports = { readConfig };
javascript
// fileService.test.js
const fs = require('fs').promises;
const { readConfig } = require('./fileService');
jest.mock('fs', () => ({
promises: {
readFile: jest.fn(),
},
}));
test('readConfig', async () => {
fs.readFile.mockResolvedValue('{"key": "value"}');
const config = await readConfig('/path/to/config.json');
expect(config).toEqual({ key: 'value' });
});
错误处理测试 #
javascript
// errorHandler.js
function errorHandler(err, req, res, next) {
console.error(err.stack);
if (err.name === 'ValidationError') {
return res.status(400).json({ error: err.message });
}
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ error: 'Unauthorized' });
}
res.status(500).json({ error: 'Internal Server Error' });
}
module.exports = errorHandler;
javascript
// errorHandler.test.js
const errorHandler = require('./errorHandler');
describe('errorHandler', () => {
let req, res, next;
beforeEach(() => {
req = {};
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
next = jest.fn();
});
test('handles ValidationError', () => {
const err = new Error('Invalid data');
err.name = 'ValidationError';
errorHandler(err, req, res, next);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: 'Invalid data' });
});
test('handles UnauthorizedError', () => {
const err = new Error('Unauthorized');
err.name = 'UnauthorizedError';
errorHandler(err, req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ error: 'Unauthorized' });
});
test('handles generic error', () => {
const err = new Error('Something went wrong');
errorHandler(err, req, res, next);
expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({ error: 'Internal Server Error' });
});
});
最佳实践 #
1. 使用测试数据库 #
javascript
beforeAll(async () => {
await mongoose.connect(process.env.TEST_DB_URL);
});
afterAll(async () => {
await mongoose.connection.close();
});
2. 清理测试数据 #
javascript
beforeEach(async () => {
await User.deleteMany({});
});
3. Mock 外部依赖 #
javascript
jest.mock('axios');
jest.mock('./external-service');
4. 测试错误情况 #
javascript
test('handles errors', async () => {
await expect(service.throwingMethod()).rejects.toThrow();
});
下一步 #
现在你已经掌握了 Node.js 测试,接下来学习 最佳实践 总结测试经验!
最后更新:2026-03-28