Appearance
Node.js 测试策略
测试是确保Node.js应用质量和稳定性的关键环节。本章详细介绍Node.js中的各种测试策略和最佳实践。
测试类型
单元测试
javascript
// math.js - 被测试的模块
function add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('参数必须是数字');
}
return a + b;
}
function multiply(a, b) {
return a * b;
}
function isEven(num) {
return num % 2 === 0;
}
module.exports = { add, multiply, isEven };
javascript
// math.test.js - 单元测试示例 (使用Jest)
const { add, multiply, isEven } = require('./math');
describe('数学函数测试', () => {
describe('add函数', () => {
test('应该正确相加两个正数', () => {
expect(add(2, 3)).toBe(5);
expect(add(0, 0)).toBe(0);
expect(add(-1, 1)).toBe(0);
});
test('应该处理小数', () => {
expect(add(0.1, 0.2)).toBeCloseTo(0.3);
});
test('应该抛出非数字参数的错误', () => {
expect(() => add('a', 'b')).toThrow('参数必须是数字');
expect(() => add(1, 'b')).toThrow('参数必须是数字');
expect(() => add(null, 2)).toThrow('参数必须是数字');
});
});
describe('multiply函数', () => {
test('应该正确相乘两个数', () => {
expect(multiply(2, 3)).toBe(6);
expect(multiply(0, 5)).toBe(0);
expect(multiply(-2, 3)).toBe(-6);
});
});
describe('isEven函数', () => {
test('应该正确判断偶数', () => {
expect(isEven(2)).toBe(true);
expect(isEven(0)).toBe(true);
expect(isEven(-2)).toBe(true);
});
test('应该正确判断奇数', () => {
expect(isEven(1)).toBe(false);
expect(isEven(3)).toBe(false);
expect(isEven(-1)).toBe(false);
});
});
});
集成测试
javascript
// user-service.js
const { add } = require('./math');
class UserService {
constructor(database) {
this.db = database;
}
async createUser(userData) {
if (!userData.name || !userData.email) {
throw new Error('姓名和邮箱是必需的');
}
// 验证邮箱格式
if (!this.isValidEmail(userData.email)) {
throw new Error('无效的邮箱格式');
}
// 计算用户ID(使用数学函数)
const id = add(Date.now(), userData.name.length);
const user = {
id,
name: userData.name,
email: userData.email,
createdAt: new Date().toISOString()
};
await this.db.insert('users', user);
return user;
}
async getUserById(id) {
return await this.db.find('users', { id });
}
isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}
module.exports = UserService;
javascript
// user-service.integration.test.js
const UserService = require('./user-service');
// 模拟数据库
const mockDatabase = {
insert: jest.fn(),
find: jest.fn()
};
describe('UserService集成测试', () => {
let userService;
beforeEach(() => {
userService = new UserService(mockDatabase);
jest.clearAllMocks();
});
describe('createUser方法', () => {
test('应该成功创建用户', async () => {
const userData = {
name: '张三',
email: 'zhang@example.com'
};
mockDatabase.insert.mockResolvedValueOnce();
const result = await userService.createUser(userData);
expect(result).toHaveProperty('id');
expect(result.name).toBe('张三');
expect(result.email).toBe('zhang@example.com');
expect(result).toHaveProperty('createdAt');
expect(mockDatabase.insert).toHaveBeenCalledWith('users', expect.objectContaining({
name: '张三',
email: 'zhang@example.com'
}));
});
test('应该验证邮箱格式', async () => {
const invalidUserData = {
name: '李四',
email: 'invalid-email'
};
await expect(userService.createUser(invalidUserData))
.rejects
.toThrow('无效的邮箱格式');
});
test('应该验证必需字段', async () => {
const incompleteUserData = {
name: '王五'
// 缺少email字段
};
await expect(userService.createUser(incompleteUserData))
.rejects
.toThrow('姓名和邮箱是必需的');
});
});
describe('getUserById方法', () => {
test('应该根据ID获取用户', async () => {
const mockUser = {
id: 123,
name: '赵六',
email: 'zhao@example.com'
};
mockDatabase.find.mockResolvedValueOnce(mockUser);
const result = await userService.getUserById(123);
expect(result).toEqual(mockUser);
expect(mockDatabase.find).toHaveBeenCalledWith('users', { id: 123 });
});
});
});
测试框架
Jest测试框架
javascript
// 安装: npm install --save-dev jest
// jest.config.js
module.exports = {
testEnvironment: 'node',
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
testMatch: [
'**/__tests__/**/*.[jt]s?(x)',
'**/?(*.)+(spec|test).[tj]s?(x)'
],
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js',
'!src/**/*.spec.js'
]
};
javascript
// 异步测试示例
describe('异步操作测试', () => {
// 模拟异步操作
function asyncOperation(value, delay = 100) {
return new Promise(resolve => {
setTimeout(() => resolve(value), delay);
});
}
test('应该等待异步操作完成', async () => {
const result = await asyncOperation('test');
expect(result).toBe('test');
});
test('应该处理异步错误', async () => {
const failingAsyncOperation = () => {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('异步错误')), 100);
});
};
await expect(failingAsyncOperation()).rejects.toThrow('异步错误');
});
// 使用done回调
test('应该使用done回调测试异步操作', (done) => {
asyncOperation('callback-test').then(result => {
expect(result).toBe('callback-test');
done();
});
});
});
测试Mock和Spy
javascript
// api-client.js
class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async get(endpoint) {
const response = await fetch(`${this.baseURL}${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
async post(endpoint, data) {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
}
module.exports = ApiClient;
javascript
// api-client.test.js
const ApiClient = require('./api-client');
describe('ApiClient测试', () => {
let apiClient;
let mockFetch;
beforeEach(() => {
mockFetch = jest.fn();
global.fetch = mockFetch;
apiClient = new ApiClient('https://api.example.com');
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('get方法', () => {
test('应该正确获取数据', async () => {
const mockData = { id: 1, name: '测试' };
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockData
});
const result = await apiClient.get('/users/1');
expect(result).toEqual(mockData);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/users/1');
});
test('应该处理HTTP错误', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found'
});
await expect(apiClient.get('/users/999'))
.rejects
.toThrow('HTTP 404: Not Found');
});
});
describe('post方法', () => {
test('应该正确发送POST请求', async () => {
const testData = { name: '新用户' };
const mockResponse = { id: 123, ...testData };
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse
});
const result = await apiClient.post('/users', testData);
expect(result).toEqual(mockResponse);
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/users',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(testData)
})
);
});
});
});
API测试
使用Supertest进行HTTP测试
bash
# 安装: npm install --save-dev supertest
javascript
// app.js
const express = require('express');
const app = express();
app.use(express.json());
let users = [
{ id: 1, name: '张三', email: 'zhang@example.com' },
{ id: 2, name: '李四', email: 'li@example.com' }
];
app.get('/api/users', (req, res) => {
const { page = 1, limit = 10 } = req.query;
const startIndex = (page - 1) * limit;
const endIndex = page * limit;
res.json({
users: users.slice(startIndex, endIndex),
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: users.length
}
});
});
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: '用户不存在' });
}
res.json(user);
});
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: '姓名和邮箱是必需的' });
}
const newUser = {
id: users.length > 0 ? Math.max(...users.map(u => u.id)) + 1 : 1,
name,
email
};
users.push(newUser);
res.status(201).json(newUser);
});
module.exports = app;
javascript
// app.test.js
const request = require('supertest');
const app = require('./app');
describe('API测试', () => {
describe('GET /api/users', () => {
test('应该返回用户列表', async () => {
const response = await request(app)
.get('/api/users')
.expect(200);
expect(response.body).toHaveProperty('users');
expect(response.body).toHaveProperty('pagination');
expect(Array.isArray(response.body.users)).toBe(true);
expect(response.body.users.length).toBeGreaterThan(0);
});
test('应该支持分页参数', async () => {
const response = await request(app)
.get('/api/users?page=1&limit=1')
.expect(200);
expect(response.body.pagination.page).toBe(1);
expect(response.body.pagination.limit).toBe(1);
expect(response.body.users.length).toBe(1);
});
});
describe('GET /api/users/:id', () => {
test('应该返回特定用户', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200);
expect(response.body).toEqual({
id: 1,
name: '张三',
email: 'zhang@example.com'
});
});
test('应该返回404当用户不存在时', async () => {
const response = await request(app)
.get('/api/users/999')
.expect(404);
expect(response.body).toEqual({ error: '用户不存在' });
});
});
describe('POST /api/users', () => {
test('应该创建新用户', async () => {
const newUser = {
name: '王五',
email: 'wang@example.com'
};
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe('王五');
expect(response.body.email).toBe('wang@example.com');
});
test('应该验证必需字段', async () => {
const invalidUser = {
name: '赵六'
// 缺少email
};
const response = await request(app)
.post('/api/users')
.send(invalidUser)
.expect(400);
expect(response.body).toEqual({ error: '姓名和邮箱是必需的' });
});
});
});
数据库测试
使用内存数据库进行测试
javascript
// database.js
const sqlite3 = require('sqlite3').verbose();
class Database {
constructor(filename = ':memory:') {
this.db = new sqlite3.Database(filename);
this.init();
}
init() {
this.db.run(`
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
}
async createUser(userData) {
return new Promise((resolve, reject) => {
this.db.run(
'INSERT INTO users (name, email) VALUES (?, ?)',
[userData.name, userData.email],
function(err) {
if (err) {
reject(err);
} else {
resolve({ id: this.lastID, ...userData });
}
}
);
});
}
async getUserById(id) {
return new Promise((resolve, reject) => {
this.db.get(
'SELECT * FROM users WHERE id = ?',
[id],
(err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
}
);
});
}
close() {
this.db.close();
}
}
module.exports = Database;
javascript
// database.test.js
const Database = require('./database');
describe('Database测试', () => {
let db;
beforeEach(async () => {
// 使用内存数据库进行测试
db = new Database();
// 插入测试数据
await db.createUser({ name: '张三', email: 'zhang@example.com' });
await db.createUser({ name: '李四', email: 'li@example.com' });
});
afterEach(() => {
db.close();
});
test('应该创建用户', async () => {
const newUser = await db.createUser({
name: '王五',
email: 'wang@example.com'
});
expect(newUser).toHaveProperty('id');
expect(newUser.name).toBe('王五');
expect(newUser.email).toBe('wang@example.com');
});
test('应该获取用户', async () => {
const user = await db.getUserById(1);
expect(user).toEqual({
id: 1,
name: '张三',
email: 'zhang@example.com',
// created_at 字段会被自动添加
});
});
test('应该处理不存在的用户', async () => {
const user = await db.getUserById(999);
expect(user).toBeUndefined();
});
});
测试覆盖率
配置Jest覆盖率
javascript
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --maxWorkers=2"
},
"devDependencies": {
"jest": "^28.0.0",
"supertest": "^6.0.0"
}
}
javascript
// 覆盖率测试示例
describe('分支覆盖率测试', () => {
function processUser(user) {
if (!user) {
return { error: '用户不存在' }; // 分支1
}
if (!user.email) {
return { error: '邮箱是必需的' }; // 分支2
}
if (!user.name) {
return { error: '姓名是必需的' }; // 分支3
}
return { success: true, user }; // 分支4
}
test('应该测试所有分支', () => {
// 测试分支1: user为null
expect(processUser(null)).toEqual({ error: '用户不存在' });
// 测试分支2: user存在但email为空
expect(processUser({})).toEqual({ error: '邮箱是必需的' });
// 测试分支3: user和email存在但name为空
expect(processUser({ email: 'test@example.com' })).toEqual({ error: '姓名是必需的' });
// 测试分支4: 所有条件都满足
const validUser = { name: '测试', email: 'test@example.com' };
expect(processUser(validUser)).toEqual({ success: true, user: validUser });
});
});
测试最佳实践
测试组织结构
project/
├── src/
│ ├── user/
│ │ ├── user.service.js
│ │ ├── user.controller.js
│ │ └── user.model.js
│ └── auth/
│ ├── auth.service.js
│ └── auth.middleware.js
├── tests/
│ ├── unit/
│ │ ├── user/
│ │ │ └── user.service.test.js
│ │ └── auth/
│ │ └── auth.service.test.js
│ ├── integration/
│ │ ├── user/
│ │ │ └── user.api.test.js
│ │ └── auth/
│ │ └── auth.api.test.js
│ └── e2e/
│ └── user-flow.test.js
└── jest.config.js
测试数据工厂
javascript
// test-utils.js
function createUser(userData = {}) {
return {
name: userData.name || '测试用户',
email: userData.email || `test${Date.now()}@example.com`,
age: userData.age || 25,
...userData
};
}
function createOrder(orderData = {}) {
return {
productId: orderData.productId || 1,
quantity: orderData.quantity || 1,
userId: orderData.userId || 1,
status: orderData.status || 'pending',
...orderData
};
}
module.exports = {
createUser,
createOrder
};
测试环境设置
javascript
// setup-test.js
const { createUser } = require('./test-utils');
// 全局测试设置
beforeAll(async () => {
// 设置测试数据库
// 启动测试服务器
});
beforeEach(async () => {
// 清理测试数据
// 重置mock
});
afterEach(async () => {
// 清理资源
jest.clearAllMocks();
});
afterAll(async () => {
// 关闭数据库连接
// 关闭测试服务器
});
// 重用测试数据
const TEST_USER = createUser({ name: 'Test User', email: 'test@example.com' });
性能测试
基准测试
javascript
// benchmark.test.js
const { performance } = require('perf_hooks');
describe('性能测试', () => {
test('应该在合理时间内完成操作', async () => {
const start = performance.now();
// 执行需要测试的函数
await someFunctionThatShouldBeFast();
const end = performance.now();
const duration = end - start;
// 断言执行时间应该小于100毫秒
expect(duration).toBeLessThan(100);
});
test('应该测试大数据集处理性能', () => {
const largeDataset = Array.from({ length: 10000 }, (_, i) => i);
const start = performance.now();
// 处理大数据集
const result = largeDataset.map(x => x * 2).filter(x => x > 100);
const end = performance.now();
const duration = end - start;
console.log(`处理10000个元素耗时: ${duration}ms`);
expect(duration).toBeLessThan(500); // 应该在500ms内完成
});
});
通过全面的测试策略,可以确保Node.js应用的质量、稳定性和可维护性。