Jest Node.js 测试 #
Node.js 测试概述 #
Node.js 后端测试主要关注 API 端点、数据库操作、业务逻辑和中间件等模块的正确性。
text
┌─────────────────────────────────────────────────────────────┐
│ Node.js 测试层次 │
├─────────────────────────────────────────────────────────────┤
│ 1. 单元测试 - 函数和模块 │
│ 2. 集成测试 - API 端点 │
│ 3. 数据库测试 - 数据库操作 │
│ 4. 中间件测试 - Express/Koa 中间件 │
│ 5. E2E 测试 - 完整流程 │
└─────────────────────────────────────────────────────────────┘
环境配置 #
安装依赖 #
bash
npm install --save-dev jest supertest @types/jest @types/supertest
Jest 配置 #
javascript
// jest.config.js
module.exports = {
testEnvironment: 'node',
testMatch: ['**/*.test.js'],
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
},
},
};
单元测试 #
工具函数测试 #
javascript
// utils/math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
module.exports = { add, subtract, multiply, divide };
// utils/math.test.js
const { add, subtract, multiply, divide } = require('./math');
describe('Math utilities', () => {
test('adds two numbers', () => {
expect(add(1, 2)).toBe(3);
});
test('subtracts two numbers', () => {
expect(subtract(5, 3)).toBe(2);
});
test('multiplies two numbers', () => {
expect(multiply(2, 3)).toBe(6);
});
test('divides two numbers', () => {
expect(divide(6, 2)).toBe(3);
});
test('throws error when dividing by zero', () => {
expect(() => divide(1, 0)).toThrow('Division by zero');
});
});
字符串工具测试 #
javascript
// utils/string.js
function capitalize(str) {
if (typeof str !== 'string') {
throw new TypeError('Expected a string');
}
return str.charAt(0).toUpperCase() + str.slice(1);
}
function truncate(str, length = 50) {
if (str.length <= length) return str;
return str.slice(0, length) + '...';
}
function isValidEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
module.exports = { capitalize, truncate, isValidEmail };
// utils/string.test.js
const { capitalize, truncate, isValidEmail } = require('./string');
describe('String utilities', () => {
describe('capitalize', () => {
test('capitalizes first letter', () => {
expect(capitalize('hello')).toBe('Hello');
});
test('handles empty string', () => {
expect(capitalize('')).toBe('');
});
test('throws error for non-string', () => {
expect(() => capitalize(123)).toThrow(TypeError);
});
});
describe('truncate', () => {
test('truncates long string', () => {
const long = 'a'.repeat(100);
expect(truncate(long, 50)).toBe('a'.repeat(50) + '...');
});
test('does not truncate short string', () => {
expect(truncate('hello', 10)).toBe('hello');
});
test('uses default length', () => {
const long = 'a'.repeat(60);
expect(truncate(long)).toBe('a'.repeat(50) + '...');
});
});
describe('isValidEmail', () => {
test('returns true for valid email', () => {
expect(isValidEmail('test@example.com')).toBe(true);
});
test('returns false for invalid email', () => {
expect(isValidEmail('invalid')).toBe(false);
expect(isValidEmail('test@')).toBe(false);
expect(isValidEmail('@example.com')).toBe(false);
});
});
});
Express API 测试 #
基础 API 测试 #
javascript
// app.js
const express = require('express');
const app = express();
app.use(express.json());
let users = [];
app.get('/api/users', (req, res) => {
res.json(users);
});
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
const user = { id: users.length + 1, name, email };
users.push(user);
res.status(201).json(user);
});
app.put('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
Object.assign(user, req.body);
res.json(user);
});
app.delete('/api/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'User not found' });
}
users.splice(index, 1);
res.status(204).send();
});
module.exports = app;
// app.test.js
const request = require('supertest');
const app = require('./app');
describe('User API', () => {
beforeEach(() => {
// 重置数据
});
describe('GET /api/users', () => {
test('returns empty array initially', async () => {
const response = await request(app).get('/api/users');
expect(response.status).toBe(200);
expect(response.body).toEqual([]);
});
});
describe('POST /api/users', () => {
test('creates a new user', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'John', email: 'john@example.com' });
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
name: 'John',
email: 'john@example.com',
});
});
test('returns 400 for missing fields', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'John' });
expect(response.status).toBe(400);
expect(response.body.error).toBe('Name and email are required');
});
});
describe('GET /api/users/:id', () => {
test('returns user by id', async () => {
// 先创建用户
const createResponse = await request(app)
.post('/api/users')
.send({ name: 'John', email: 'john@example.com' });
const response = await request(app).get(`/api/users/${createResponse.body.id}`);
expect(response.status).toBe(200);
expect(response.body.name).toBe('John');
});
test('returns 404 for non-existent user', async () => {
const response = await request(app).get('/api/users/999');
expect(response.status).toBe(404);
});
});
describe('PUT /api/users/:id', () => {
test('updates user', async () => {
const createResponse = await request(app)
.post('/api/users')
.send({ name: 'John', email: 'john@example.com' });
const response = await request(app)
.put(`/api/users/${createResponse.body.id}`)
.send({ name: 'Jane' });
expect(response.status).toBe(200);
expect(response.body.name).toBe('Jane');
});
});
describe('DELETE /api/users/:id', () => {
test('deletes user', async () => {
const createResponse = await request(app)
.post('/api/users')
.send({ name: 'John', email: 'john@example.com' });
const response = await request(app).delete(`/api/users/${createResponse.body.id}`);
expect(response.status).toBe(204);
});
});
});
数据库测试 #
MongoDB 测试 #
javascript
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
createdAt: { type: Date, default: Date.now },
});
module.exports = mongoose.model('User', userSchema);
// models/User.test.js
const mongoose = require('mongoose');
const User = require('./User');
describe('User model', () => {
beforeAll(async () => {
await mongoose.connect('mongodb://localhost:27017/test');
});
afterAll(async () => {
await mongoose.connection.close();
});
beforeEach(async () => {
await User.deleteMany({});
});
test('creates user successfully', async () => {
const user = await User.create({
name: 'John',
email: 'john@example.com',
});
expect(user.name).toBe('John');
expect(user.email).toBe('john@example.com');
});
test('requires name and email', async () => {
const user = new User({});
await expect(user.save()).rejects.toThrow();
});
test('enforces unique email', async () => {
await User.create({ name: 'John', email: 'john@example.com' });
await expect(
User.create({ name: 'Jane', email: 'john@example.com' })
).rejects.toThrow();
});
});
使用 mongodb-memory-server #
javascript
// jest.setup.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();
});
afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
});
中间件测试 #
认证中间件 #
javascript
// middleware/auth.js
function authMiddleware(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
if (!token.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Invalid token format' });
}
try {
const decoded = verifyToken(token.slice(7));
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
}
module.exports = authMiddleware;
// middleware/auth.test.js
const request = require('supertest');
const express = require('express');
const authMiddleware = require('./auth');
const app = express();
app.use(express.json());
app.get('/protected', authMiddleware, (req, res) => {
res.json({ user: req.user });
});
describe('Auth middleware', () => {
test('returns 401 without token', async () => {
const response = await request(app).get('/protected');
expect(response.status).toBe(401);
expect(response.body.error).toBe('No token provided');
});
test('returns 401 for invalid token format', async () => {
const response = await request(app)
.get('/protected')
.set('Authorization', 'InvalidToken');
expect(response.status).toBe(401);
expect(response.body.error).toBe('Invalid token format');
});
});
错误处理中间件 #
javascript
// middleware/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;
// middleware/errorHandler.test.js
const request = require('supertest');
const express = require('express');
const errorHandler = require('./errorHandler');
const app = express();
app.get('/validation-error', (req, res, next) => {
const err = new Error('Validation failed');
err.name = 'ValidationError';
next(err);
});
app.get('/unauthorized', (req, res, next) => {
const err = new Error('Unauthorized');
err.name = 'UnauthorizedError';
next(err);
});
app.get('/server-error', (req, res, next) => {
next(new Error('Something went wrong'));
});
app.use(errorHandler);
describe('Error handler middleware', () => {
test('handles validation error', async () => {
const response = await request(app).get('/validation-error');
expect(response.status).toBe(400);
});
test('handles unauthorized error', async () => {
const response = await request(app).get('/unauthorized');
expect(response.status).toBe(401);
});
test('handles generic error', async () => {
const response = await request(app).get('/server-error');
expect(response.status).toBe(500);
});
});
服务层测试 #
javascript
// services/userService.js
class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}
async createUser(userData) {
if (!userData.name || !userData.email) {
throw new Error('Name and email are required');
}
return this.userRepository.create(userData);
}
async getUser(id) {
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error('User not found');
}
return user;
}
async updateUser(id, userData) {
const user = await this.getUser(id);
Object.assign(user, userData);
return this.userRepository.update(user);
}
async deleteUser(id) {
await this.getUser(id);
return this.userRepository.delete(id);
}
}
module.exports = UserService;
// services/userService.test.js
const UserService = require('./userService');
describe('UserService', () => {
let userService;
let mockRepository;
beforeEach(() => {
mockRepository = {
create: jest.fn(),
findById: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};
userService = new UserService(mockRepository);
});
describe('createUser', () => {
test('creates user with valid data', async () => {
mockRepository.create.mockResolvedValue({ id: 1, name: 'John' });
const result = await userService.createUser({ name: 'John', email: 'john@example.com' });
expect(mockRepository.create).toHaveBeenCalledWith({ name: 'John', email: 'john@example.com' });
expect(result).toEqual({ id: 1, name: 'John' });
});
test('throws error for missing fields', async () => {
await expect(userService.createUser({ name: 'John' })).rejects.toThrow('Name and email are required');
});
});
describe('getUser', () => {
test('returns user by id', async () => {
mockRepository.findById.mockResolvedValue({ id: 1, name: 'John' });
const result = await userService.getUser(1);
expect(mockRepository.findById).toHaveBeenCalledWith(1);
expect(result).toEqual({ id: 1, name: 'John' });
});
test('throws error for non-existent user', async () => {
mockRepository.findById.mockResolvedValue(null);
await expect(userService.getUser(999)).rejects.toThrow('User not found');
});
});
});
最佳实践 #
1. 测试隔离 #
javascript
beforeEach(() => {
// 重置状态
jest.clearAllMocks();
});
afterEach(() => {
// 清理资源
});
2. 使用测试数据库 #
javascript
beforeAll(async () => {
await connectTestDatabase();
});
afterAll(async () => {
await disconnectTestDatabase();
});
3. Mock 外部依赖 #
javascript
jest.mock('axios');
jest.mock('nodemailer');
下一步 #
现在你已经掌握了 Node.js 后端测试,接下来学习 最佳实践 总结测试经验!
最后更新:2026-03-28