集成测试基础
📋 概述
集成测试是验证不同组件、模块或服务之间交互是否正确的测试方法。在Node.js应用中,集成测试确保数据库操作、外部API调用、文件系统访问、消息队列等各个模块能够正确协同工作,是构建可靠系统的重要环节。
🎯 学习目标
- 理解集成测试的核心概念和重要性
- 掌握Node.js集成测试的设计和实施方法
- 学会测试数据库、API、文件系统等外部依赖
- 了解集成测试的最佳实践和常见挑战
🔗 集成测试分类
集成测试层次
mermaid
graph TB
A[集成测试类型] --> B[组件集成测试<br/>Component Integration]
A --> C[系统集成测试<br/>System Integration]
A --> D[合约测试<br/>Contract Testing]
A --> E[端到端集成测试<br/>End-to-End Integration]
B --> B1[模块间接口<br/>数据流验证<br/>内部组件协作]
C --> C1[外部系统集成<br/>第三方服务<br/>基础设施依赖]
D --> D2[API合约验证<br/>消息格式约定<br/>服务间协议]
E --> E1[完整业务流程<br/>跨系统数据流<br/>用户场景验证]
style B fill:#e1f5fe
style C fill:#f3e5f5
style D fill:#e8f5e8
style E fill:#fff3e0
集成测试策略
javascript
const IntegrationTestingStrategies = {
BIG_BANG: {
name: '大爆炸集成',
description: '同时集成所有组件进行测试',
advantages: [
'实施简单快速',
'能发现整体集成问题',
'适合小型系统'
],
disadvantages: [
'故障定位困难',
'调试复杂',
'测试覆盖不全面'
],
useCase: '小型应用或原型验证'
},
INCREMENTAL: {
name: '增量集成',
description: '逐步添加组件进行集成测试',
types: {
topDown: '自顶向下集成',
bottomUp: '自底向上集成',
sandwich: '三明治集成(混合方式)'
},
advantages: [
'故障定位准确',
'测试覆盖全面',
'风险控制好'
],
disadvantages: [
'需要测试桩或驱动程序',
'实施复杂',
'时间成本高'
]
},
CONTINUOUS: {
name: '持续集成测试',
description: '在CI/CD流程中持续执行集成测试',
principles: [
'快速反馈',
'自动化执行',
'环境一致性',
'依赖管理'
],
implementation: [
'容器化测试环境',
'数据库迁移脚本',
'外部服务模拟',
'测试数据管理'
]
}
};
🗄️ 数据库集成测试
数据库测试设置
javascript
// database-integration-test-setup.js
const { Pool } = require('pg');
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
class DatabaseTestSetup {
constructor() {
this.pgPool = null;
this.mongoServer = null;
this.testDatabases = [];
}
// PostgreSQL测试设置
async setupPostgreSQL() {
const testConfig = {
user: process.env.TEST_DB_USER || 'test_user',
host: process.env.TEST_DB_HOST || 'localhost',
database: `test_db_${Date.now()}`,
password: process.env.TEST_DB_PASSWORD || 'test_password',
port: process.env.TEST_DB_PORT || 5432,
};
// 创建测试数据库
const adminPool = new Pool({
...testConfig,
database: 'postgres' // 连接到默认数据库创建测试数据库
});
try {
await adminPool.query(`CREATE DATABASE "${testConfig.database}"`);
console.log(`Test database created: ${testConfig.database}`);
} catch (error) {
console.warn('Database may already exist:', error.message);
} finally {
await adminPool.end();
}
// 连接到测试数据库
this.pgPool = new Pool(testConfig);
this.testDatabases.push({
type: 'postgresql',
name: testConfig.database,
pool: this.pgPool
});
// 运行迁移脚本
await this.runMigrations(this.pgPool);
return this.pgPool;
}
// MongoDB测试设置
async setupMongoDB() {
this.mongoServer = await MongoMemoryServer.create({
instance: {
dbName: `test_db_${Date.now()}`
}
});
const mongoUri = this.mongoServer.getUri();
await mongoose.connect(mongoUri);
this.testDatabases.push({
type: 'mongodb',
uri: mongoUri,
server: this.mongoServer
});
console.log('MongoDB test server started:', mongoUri);
return mongoose.connection;
}
// 运行数据库迁移
async runMigrations(pool) {
const migrations = [
`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`,
`
CREATE TABLE IF NOT EXISTS products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
category_id INTEGER,
stock_quantity INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`,
`
CREATE TABLE IF NOT EXISTS orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
total_amount DECIMAL(10,2) NOT NULL,
status VARCHAR(50) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`,
`
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id);
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
`
];
for (const migration of migrations) {
try {
await pool.query(migration);
} catch (error) {
console.warn('Migration warning:', error.message);
}
}
console.log('Database migrations completed');
}
// 插入测试数据
async seedTestData(pool) {
const testUsers = [
{ name: 'John Doe', email: 'john@test.com', password_hash: 'hash1' },
{ name: 'Jane Smith', email: 'jane@test.com', password_hash: 'hash2' },
{ name: 'Bob Johnson', email: 'bob@test.com', password_hash: 'hash3' }
];
const testProducts = [
{ name: 'Laptop', description: 'Gaming laptop', price: 999.99, stock_quantity: 10 },
{ name: 'Mouse', description: 'Wireless mouse', price: 29.99, stock_quantity: 50 },
{ name: 'Keyboard', description: 'Mechanical keyboard', price: 79.99, stock_quantity: 25 }
];
// 插入用户数据
for (const user of testUsers) {
await pool.query(
'INSERT INTO users (name, email, password_hash) VALUES ($1, $2, $3) ON CONFLICT (email) DO NOTHING',
[user.name, user.email, user.password_hash]
);
}
// 插入产品数据
for (const product of testProducts) {
await pool.query(
'INSERT INTO products (name, description, price, stock_quantity) VALUES ($1, $2, $3, $4)',
[product.name, product.description, product.price, product.stock_quantity]
);
}
console.log('Test data seeded successfully');
}
// 清理测试数据
async cleanupTestData(pool) {
const tables = ['orders', 'products', 'users'];
for (const table of tables) {
await pool.query(`TRUNCATE TABLE ${table} RESTART IDENTITY CASCADE`);
}
console.log('Test data cleaned up');
}
// 清理所有测试资源
async cleanup() {
for (const db of this.testDatabases) {
try {
if (db.type === 'postgresql' && db.pool) {
await db.pool.end();
console.log(`PostgreSQL pool closed for: ${db.name}`);
}
if (db.type === 'mongodb' && db.server) {
await mongoose.disconnect();
await db.server.stop();
console.log('MongoDB test server stopped');
}
} catch (error) {
console.warn(`Error cleaning up ${db.type}:`, error.message);
}
}
this.testDatabases = [];
}
}
module.exports = DatabaseTestSetup;
数据库集成测试示例
javascript
// user-repository.integration.test.js
const DatabaseTestSetup = require('./database-integration-test-setup');
const UserRepository = require('../src/repositories/user-repository');
describe('UserRepository Integration Tests', () => {
let dbSetup;
let pool;
let userRepository;
beforeAll(async () => {
dbSetup = new DatabaseTestSetup();
pool = await dbSetup.setupPostgreSQL();
userRepository = new UserRepository(pool);
});
afterAll(async () => {
await dbSetup.cleanup();
});
beforeEach(async () => {
await dbSetup.cleanupTestData(pool);
await dbSetup.seedTestData(pool);
});
describe('createUser', () => {
it('should create a new user in database', async () => {
const userData = {
name: 'New User',
email: 'newuser@test.com',
password: 'password123'
};
const createdUser = await userRepository.createUser(userData);
expect(createdUser).toMatchObject({
id: expect.any(Number),
name: userData.name,
email: userData.email,
created_at: expect.any(Date)
});
expect(createdUser.password_hash).toBeDefined();
expect(createdUser.password_hash).not.toBe(userData.password);
// 验证数据库中的记录
const result = await pool.query('SELECT * FROM users WHERE id = $1', [createdUser.id]);
expect(result.rows).toHaveLength(1);
expect(result.rows[0].email).toBe(userData.email);
});
it('should throw error when creating user with duplicate email', async () => {
const userData = {
name: 'Duplicate User',
email: 'john@test.com', // 已存在的邮箱
password: 'password123'
};
await expect(userRepository.createUser(userData))
.rejects
.toThrow('Email already exists');
});
it('should hash password before storing', async () => {
const userData = {
name: 'Security Test User',
email: 'security@test.com',
password: 'plaintext-password'
};
const createdUser = await userRepository.createUser(userData);
// 验证密码已被哈希
expect(createdUser.password_hash).not.toBe(userData.password);
expect(createdUser.password_hash).toMatch(/^\$2[ayb]\$\d{1,2}\$/); // bcrypt格式
// 验证密码可以正确验证
const isValid = await userRepository.verifyPassword(userData.password, createdUser.password_hash);
expect(isValid).toBe(true);
});
});
describe('findUserById', () => {
it('should find existing user by id', async () => {
// 获取种子数据中的用户
const result = await pool.query('SELECT * FROM users WHERE email = $1', ['john@test.com']);
const seedUser = result.rows[0];
const foundUser = await userRepository.findUserById(seedUser.id);
expect(foundUser).toMatchObject({
id: seedUser.id,
name: seedUser.name,
email: seedUser.email
});
});
it('should return null for non-existent user', async () => {
const nonExistentId = 99999;
const foundUser = await userRepository.findUserById(nonExistentId);
expect(foundUser).toBeNull();
});
});
describe('updateUser', () => {
it('should update user information', async () => {
// 获取种子数据中的用户
const result = await pool.query('SELECT * FROM users WHERE email = $1', ['jane@test.com']);
const existingUser = result.rows[0];
const updateData = {
name: 'Jane Updated',
email: 'jane.updated@test.com'
};
const updatedUser = await userRepository.updateUser(existingUser.id, updateData);
expect(updatedUser).toMatchObject({
id: existingUser.id,
name: updateData.name,
email: updateData.email,
updated_at: expect.any(Date)
});
// 验证数据库中的更新
const verifyResult = await pool.query('SELECT * FROM users WHERE id = $1', [existingUser.id]);
expect(verifyResult.rows[0].name).toBe(updateData.name);
expect(verifyResult.rows[0].email).toBe(updateData.email);
});
it('should not allow updating to duplicate email', async () => {
const result1 = await pool.query('SELECT * FROM users WHERE email = $1', ['john@test.com']);
const result2 = await pool.query('SELECT * FROM users WHERE email = $1', ['jane@test.com']);
const user1 = result1.rows[0];
const user2 = result2.rows[0];
await expect(userRepository.updateUser(user1.id, { email: user2.email }))
.rejects
.toThrow('Email already exists');
});
});
describe('deleteUser', () => {
it('should delete user and return deletion count', async () => {
const result = await pool.query('SELECT * FROM users WHERE email = $1', ['bob@test.com']);
const userToDelete = result.rows[0];
const deletedCount = await userRepository.deleteUser(userToDelete.id);
expect(deletedCount).toBe(1);
// 验证用户已被删除
const verifyResult = await pool.query('SELECT * FROM users WHERE id = $1', [userToDelete.id]);
expect(verifyResult.rows).toHaveLength(0);
});
it('should return 0 when deleting non-existent user', async () => {
const nonExistentId = 99999;
const deletedCount = await userRepository.deleteUser(nonExistentId);
expect(deletedCount).toBe(0);
});
});
describe('transaction handling', () => {
it('should rollback transaction on error', async () => {
const initialUserCount = await pool.query('SELECT COUNT(*) FROM users');
const initialCount = parseInt(initialUserCount.rows[0].count);
try {
await userRepository.createUsersTransaction([
{ name: 'User 1', email: 'user1@test.com', password: 'pass123' },
{ name: 'User 2', email: 'john@test.com', password: 'pass123' }, // 重复邮箱
{ name: 'User 3', email: 'user3@test.com', password: 'pass123' }
]);
} catch (error) {
// 期望错误
}
// 验证事务回滚,用户数量不变
const finalUserCount = await pool.query('SELECT COUNT(*) FROM users');
const finalCount = parseInt(finalUserCount.rows[0].count);
expect(finalCount).toBe(initialCount);
});
it('should commit transaction when all operations succeed', async () => {
const initialUserCount = await pool.query('SELECT COUNT(*) FROM users');
const initialCount = parseInt(initialUserCount.rows[0].count);
const users = await userRepository.createUsersTransaction([
{ name: 'Batch User 1', email: 'batch1@test.com', password: 'pass123' },
{ name: 'Batch User 2', email: 'batch2@test.com', password: 'pass123' }
]);
expect(users).toHaveLength(2);
// 验证事务提交,用户数量增加
const finalUserCount = await pool.query('SELECT COUNT(*) FROM users');
const finalCount = parseInt(finalUserCount.rows[0].count);
expect(finalCount).toBe(initialCount + 2);
});
});
});
🌐 外部API集成测试
HTTP客户端集成测试
javascript
// api-client.integration.test.js
const nock = require('nock');
const ApiClient = require('../src/services/api-client');
describe('ApiClient Integration Tests', () => {
let apiClient;
const baseURL = 'https://api.example.com';
beforeEach(() => {
apiClient = new ApiClient(baseURL);
nock.cleanAll();
});
afterEach(() => {
nock.cleanAll();
});
describe('getUser', () => {
it('should successfully fetch user data', async () => {
const userId = 123;
const userData = {
id: userId,
name: 'John Doe',
email: 'john@example.com'
};
nock(baseURL)
.get(`/users/${userId}`)
.reply(200, userData);
const result = await apiClient.getUser(userId);
expect(result).toEqual(userData);
});
it('should handle 404 errors gracefully', async () => {
const userId = 999;
nock(baseURL)
.get(`/users/${userId}`)
.reply(404, { error: 'User not found' });
await expect(apiClient.getUser(userId))
.rejects
.toThrow('User not found');
});
it('should retry on network failures', async () => {
const userId = 123;
const userData = { id: userId, name: 'John Doe' };
// 第一次请求失败
nock(baseURL)
.get(`/users/${userId}`)
.replyWithError('Network error');
// 第二次请求成功
nock(baseURL)
.get(`/users/${userId}`)
.reply(200, userData);
const result = await apiClient.getUser(userId);
expect(result).toEqual(userData);
});
it('should handle timeout errors', async () => {
const userId = 123;
nock(baseURL)
.get(`/users/${userId}`)
.delay(6000) // 延迟6秒,超过5秒超时设置
.reply(200, {});
await expect(apiClient.getUser(userId))
.rejects
.toThrow('timeout');
});
});
describe('createUser', () => {
it('should create user with proper request format', async () => {
const userData = {
name: 'Jane Doe',
email: 'jane@example.com'
};
const createdUser = {
id: 456,
...userData,
createdAt: '2023-01-01T00:00:00.000Z'
};
const scope = nock(baseURL)
.post('/users', userData)
.reply(201, createdUser);
const result = await apiClient.createUser(userData);
expect(result).toEqual(createdUser);
expect(scope.isDone()).toBe(true);
});
it('should include authentication headers', async () => {
const userData = { name: 'Test User', email: 'test@example.com' };
const authToken = 'bearer-token-123';
apiClient.setAuthToken(authToken);
const scope = nock(baseURL)
.post('/users')
.matchHeader('Authorization', `Bearer ${authToken}`)
.reply(201, { id: 789, ...userData });
await apiClient.createUser(userData);
expect(scope.isDone()).toBe(true);
});
it('should handle validation errors', async () => {
const invalidUserData = {
name: '', // 空名称
email: 'invalid-email' // 无效邮箱
};
const validationErrors = {
errors: [
{ field: 'name', message: 'Name is required' },
{ field: 'email', message: 'Invalid email format' }
]
};
nock(baseURL)
.post('/users', invalidUserData)
.reply(400, validationErrors);
await expect(apiClient.createUser(invalidUserData))
.rejects
.toThrow('Validation failed');
});
});
describe('rate limiting', () => {
it('should handle rate limit responses', async () => {
const userId = 123;
nock(baseURL)
.get(`/users/${userId}`)
.reply(429, { error: 'Rate limit exceeded' }, {
'Retry-After': '60'
});
await expect(apiClient.getUser(userId))
.rejects
.toThrow('Rate limit exceeded');
});
it('should respect rate limit and retry after delay', async () => {
const userId = 123;
const userData = { id: userId, name: 'John Doe' };
// 第一次请求被限流
nock(baseURL)
.get(`/users/${userId}`)
.reply(429, { error: 'Rate limit exceeded' }, {
'Retry-After': '1' // 1秒后重试
});
// 第二次请求成功
nock(baseURL)
.get(`/users/${userId}`)
.reply(200, userData);
const startTime = Date.now();
const result = await apiClient.getUser(userId);
const endTime = Date.now();
expect(result).toEqual(userData);
expect(endTime - startTime).toBeGreaterThan(1000); // 确认等待了至少1秒
});
});
describe('pagination', () => {
it('should handle paginated responses', async () => {
const page1Data = {
users: [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }
],
pagination: {
page: 1,
perPage: 2,
total: 5,
hasNext: true
}
};
const page2Data = {
users: [
{ id: 3, name: 'User 3' },
{ id: 4, name: 'User 4' }
],
pagination: {
page: 2,
perPage: 2,
total: 5,
hasNext: true
}
};
nock(baseURL)
.get('/users?page=1&limit=2')
.reply(200, page1Data);
nock(baseURL)
.get('/users?page=2&limit=2')
.reply(200, page2Data);
const result1 = await apiClient.getUsers({ page: 1, limit: 2 });
const result2 = await apiClient.getUsers({ page: 2, limit: 2 });
expect(result1.users).toHaveLength(2);
expect(result1.pagination.hasNext).toBe(true);
expect(result2.users).toHaveLength(2);
expect(result2.pagination.page).toBe(2);
});
it('should auto-fetch all pages when requested', async () => {
// 模拟多页数据
nock(baseURL)
.get('/users?page=1&limit=10')
.reply(200, {
users: Array.from({ length: 10 }, (_, i) => ({ id: i + 1, name: `User ${i + 1}` })),
pagination: { page: 1, perPage: 10, total: 25, hasNext: true }
});
nock(baseURL)
.get('/users?page=2&limit=10')
.reply(200, {
users: Array.from({ length: 10 }, (_, i) => ({ id: i + 11, name: `User ${i + 11}` })),
pagination: { page: 2, perPage: 10, total: 25, hasNext: true }
});
nock(baseURL)
.get('/users?page=3&limit=10')
.reply(200, {
users: Array.from({ length: 5 }, (_, i) => ({ id: i + 21, name: `User ${i + 21}` })),
pagination: { page: 3, perPage: 10, total: 25, hasNext: false }
});
const allUsers = await apiClient.getAllUsers();
expect(allUsers).toHaveLength(25);
expect(allUsers[0].id).toBe(1);
expect(allUsers[24].id).toBe(25);
});
});
});
第三方服务集成测试
javascript
// payment-service.integration.test.js
const PaymentService = require('../src/services/payment-service');
const nock = require('nock');
describe('PaymentService Integration Tests', () => {
let paymentService;
const stripeBaseURL = 'https://api.stripe.com';
beforeEach(() => {
paymentService = new PaymentService({
stripeApiKey: 'sk_test_fake_key',
environment: 'test'
});
nock.cleanAll();
});
afterEach(() => {
nock.cleanAll();
});
describe('createPaymentIntent', () => {
it('should create payment intent successfully', async () => {
const paymentData = {
amount: 2000, // $20.00
currency: 'usd',
customer: 'cus_test123'
};
const mockResponse = {
id: 'pi_test123',
amount: 2000,
currency: 'usd',
status: 'requires_confirmation',
client_secret: 'pi_test123_secret'
};
nock(stripeBaseURL)
.post('/v1/payment_intents')
.reply(200, mockResponse);
const result = await paymentService.createPaymentIntent(paymentData);
expect(result).toMatchObject({
id: 'pi_test123',
amount: 2000,
currency: 'usd',
status: 'requires_confirmation'
});
});
it('should handle authentication errors', async () => {
nock(stripeBaseURL)
.post('/v1/payment_intents')
.reply(401, {
error: {
type: 'invalid_request_error',
message: 'Invalid API key provided'
}
});
await expect(paymentService.createPaymentIntent({ amount: 1000, currency: 'usd' }))
.rejects
.toThrow('Invalid API key provided');
});
it('should handle card declined errors', async () => {
const paymentData = {
amount: 1000,
currency: 'usd',
payment_method: 'pm_card_declined'
};
nock(stripeBaseURL)
.post('/v1/payment_intents')
.reply(402, {
error: {
type: 'card_error',
code: 'card_declined',
message: 'Your card was declined.'
}
});
await expect(paymentService.createPaymentIntent(paymentData))
.rejects
.toThrow('Your card was declined.');
});
});
describe('webhook handling', () => {
it('should verify and process webhook events', async () => {
const webhookPayload = {
id: 'evt_test123',
type: 'payment_intent.succeeded',
data: {
object: {
id: 'pi_test123',
status: 'succeeded',
amount: 2000
}
}
};
const signature = 'test_signature';
const webhookSecret = 'whsec_test';
// Mock Stripe's webhook signature verification
jest.spyOn(paymentService, 'verifyWebhookSignature')
.mockReturnValue(true);
const result = await paymentService.processWebhook(
JSON.stringify(webhookPayload),
signature,
webhookSecret
);
expect(result).toMatchObject({
processed: true,
eventType: 'payment_intent.succeeded',
paymentIntentId: 'pi_test123'
});
});
it('should reject invalid webhook signatures', async () => {
const webhookPayload = { id: 'evt_test123', type: 'test' };
const invalidSignature = 'invalid_signature';
const webhookSecret = 'whsec_test';
jest.spyOn(paymentService, 'verifyWebhookSignature')
.mockReturnValue(false);
await expect(paymentService.processWebhook(
JSON.stringify(webhookPayload),
invalidSignature,
webhookSecret
)).rejects.toThrow('Invalid webhook signature');
});
});
describe('error handling and retries', () => {
it('should retry on temporary failures', async () => {
const paymentData = { amount: 1000, currency: 'usd' };
const successResponse = {
id: 'pi_retry_success',
amount: 1000,
currency: 'usd',
status: 'requires_confirmation'
};
// 第一次请求失败(网络错误)
nock(stripeBaseURL)
.post('/v1/payment_intents')
.replyWithError('Network error');
// 第二次请求成功
nock(stripeBaseURL)
.post('/v1/payment_intents')
.reply(200, successResponse);
const result = await paymentService.createPaymentIntent(paymentData);
expect(result.id).toBe('pi_retry_success');
});
it('should not retry on permanent failures', async () => {
const paymentData = { amount: -1000, currency: 'usd' }; // 无效金额
nock(stripeBaseURL)
.post('/v1/payment_intents')
.reply(400, {
error: {
type: 'invalid_request_error',
message: 'Amount must be positive'
}
});
await expect(paymentService.createPaymentIntent(paymentData))
.rejects
.toThrow('Amount must be positive');
// 验证只调用了一次,没有重试
expect(nock.pendingMocks()).toHaveLength(0);
});
});
});
📁 文件系统集成测试
文件操作集成测试
javascript
// file-service.integration.test.js
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
const FileService = require('../src/services/file-service');
describe('FileService Integration Tests', () => {
let fileService;
let testDir;
beforeEach(async () => {
// 创建临时测试目录
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fileservice-test-'));
fileService = new FileService({
baseDir: testDir,
maxFileSize: 10 * 1024 * 1024, // 10MB
allowedExtensions: ['.txt', '.json', '.csv', '.pdf']
});
});
afterEach(async () => {
// 清理测试目录
try {
await fs.rmdir(testDir, { recursive: true });
} catch (error) {
console.warn('Failed to cleanup test directory:', error.message);
}
});
describe('saveFile', () => {
it('should save file to correct location', async () => {
const fileName = 'test-file.txt';
const fileContent = 'Hello, World!';
const buffer = Buffer.from(fileContent, 'utf8');
const result = await fileService.saveFile(fileName, buffer);
expect(result).toMatchObject({
fileName,
filePath: expect.stringContaining(fileName),
size: buffer.length,
mimeType: 'text/plain'
});
// 验证文件确实被保存
const savedContent = await fs.readFile(result.filePath, 'utf8');
expect(savedContent).toBe(fileContent);
});
it('should generate unique filename for duplicates', async () => {
const fileName = 'duplicate.txt';
const content1 = 'First file';
const content2 = 'Second file';
const result1 = await fileService.saveFile(fileName, Buffer.from(content1));
const result2 = await fileService.saveFile(fileName, Buffer.from(content2));
expect(result1.fileName).toBe('duplicate.txt');
expect(result2.fileName).toMatch(/^duplicate-\d+\.txt$/);
// 验证两个文件都存在且内容不同
const content1Saved = await fs.readFile(result1.filePath, 'utf8');
const content2Saved = await fs.readFile(result2.filePath, 'utf8');
expect(content1Saved).toBe(content1);
expect(content2Saved).toBe(content2);
});
it('should reject files with disallowed extensions', async () => {
const fileName = 'malicious.exe';
const buffer = Buffer.from('malicious content');
await expect(fileService.saveFile(fileName, buffer))
.rejects
.toThrow('File extension not allowed');
});
it('should reject files exceeding size limit', async () => {
const fileName = 'large-file.txt';
const largeBuffer = Buffer.alloc(15 * 1024 * 1024); // 15MB
await expect(fileService.saveFile(fileName, largeBuffer))
.rejects
.toThrow('File size exceeds limit');
});
});
describe('readFile', () => {
it('should read existing file', async () => {
const fileName = 'read-test.txt';
const originalContent = 'Content to read';
// 先保存文件
await fileService.saveFile(fileName, Buffer.from(originalContent));
// 然后读取文件
const readResult = await fileService.readFile(fileName);
expect(readResult.content.toString('utf8')).toBe(originalContent);
expect(readResult.metadata).toMatchObject({
fileName,
size: originalContent.length,
mimeType: 'text/plain'
});
});
it('should throw error for non-existent file', async () => {
await expect(fileService.readFile('non-existent.txt'))
.rejects
.toThrow('File not found');
});
});
describe('deleteFile', () => {
it('should delete existing file', async () => {
const fileName = 'delete-test.txt';
const content = 'File to delete';
// 保存文件
const saveResult = await fileService.saveFile(fileName, Buffer.from(content));
// 验证文件存在
expect(await fileService.fileExists(fileName)).toBe(true);
// 删除文件
const deleteResult = await fileService.deleteFile(fileName);
expect(deleteResult.deleted).toBe(true);
// 验证文件不存在
expect(await fileService.fileExists(fileName)).toBe(false);
});
it('should handle deletion of non-existent file gracefully', async () => {
const result = await fileService.deleteFile('non-existent.txt');
expect(result.deleted).toBe(false);
expect(result.reason).toBe('File not found');
});
});
describe('listFiles', () => {
it('should list files in directory', async () => {
const files = [
{ name: 'file1.txt', content: 'Content 1' },
{ name: 'file2.json', content: '{"key": "value"}' },
{ name: 'file3.csv', content: 'col1,col2\\nval1,val2' }
];
// 保存多个文件
for (const file of files) {
await fileService.saveFile(file.name, Buffer.from(file.content));
}
const fileList = await fileService.listFiles();
expect(fileList).toHaveLength(3);
expect(fileList.map(f => f.fileName)).toEqual(
expect.arrayContaining(['file1.txt', 'file2.json', 'file3.csv'])
);
fileList.forEach(file => {
expect(file).toMatchObject({
fileName: expect.any(String),
size: expect.any(Number),
mimeType: expect.any(String),
createdAt: expect.any(Date),
modifiedAt: expect.any(Date)
});
});
});
it('should filter files by extension', async () => {
const files = [
{ name: 'document.txt', content: 'Text content' },
{ name: 'data.json', content: '{}' },
{ name: 'another.txt', content: 'More text' }
];
for (const file of files) {
await fileService.saveFile(file.name, Buffer.from(file.content));
}
const txtFiles = await fileService.listFiles({ extension: '.txt' });
expect(txtFiles).toHaveLength(2);
expect(txtFiles.every(f => f.fileName.endsWith('.txt'))).toBe(true);
});
});
describe('file operations with subdirectories', () => {
it('should create and manage subdirectories', async () => {
const subDirPath = 'uploads/images';
const fileName = 'image.txt'; // 简化测试用文本文件
const content = 'Image placeholder';
const result = await fileService.saveFile(
fileName,
Buffer.from(content),
{ subDirectory: subDirPath }
);
expect(result.filePath).toMatch(/uploads[/\\]images[/\\]image\.txt$/);
// 验证子目录被创建
const fullSubDirPath = path.join(testDir, subDirPath);
const stat = await fs.stat(fullSubDirPath);
expect(stat.isDirectory()).toBe(true);
// 验证文件在正确位置
const savedContent = await fs.readFile(result.filePath, 'utf8');
expect(savedContent).toBe(content);
});
});
describe('concurrent file operations', () => {
it('should handle concurrent file saves', async () => {
const concurrentSaves = Array.from({ length: 10 }, (_, i) =>
fileService.saveFile(`concurrent-${i}.txt`, Buffer.from(`Content ${i}`))
);
const results = await Promise.all(concurrentSaves);
expect(results).toHaveLength(10);
results.forEach((result, i) => {
expect(result.fileName).toBe(`concurrent-${i}.txt`);
});
// 验证所有文件都被正确保存
const fileList = await fileService.listFiles();
expect(fileList).toHaveLength(10);
});
});
});
📝 集成测试最佳实践
测试环境隔离
javascript
const IntegrationTestBestPractices = {
ENVIRONMENT_ISOLATION: {
testContainers: [
'使用Docker容器隔离测试环境',
'每个测试套件使用独立的容器实例',
'自动化容器的启动和清理过程',
'确保测试环境的一致性和可重现性'
],
databaseIsolation: [
'为每个测试创建独立的数据库',
'使用事务回滚清理测试数据',
'内存数据库用于快速测试',
'种子数据的一致性管理'
],
networkIsolation: [
'使用测试专用的网络端口',
'模拟外部服务调用',
'网络延迟和错误的模拟',
'并发访问的控制'
]
},
DATA_MANAGEMENT: {
testDataStrategy: [
'使用工厂模式生成测试数据',
'建立测试数据的层次结构',
'确保测试数据的完整性',
'支持复杂业务场景的数据准备'
],
cleanupStrategy: [
'每个测试后清理数据',
'避免测试间的数据污染',
'级联删除相关数据',
'性能优化的批量操作'
]
},
ERROR_HANDLING: {
resilience: [
'测试各种错误情况',
'验证错误恢复机制',
'测试超时和重试逻辑',
'资源泄漏的检测'
],
monitoring: [
'集成测试执行时间监控',
'资源使用情况追踪',
'失败模式分析',
'测试稳定性指标'
]
}
};
📝 总结
集成测试是确保Node.js应用组件协作正确的关键测试环节:
- 全面覆盖:数据库、API、文件系统等外部依赖的集成验证
- 真实环境:模拟生产环境的配置和数据流
- 错误处理:验证异常情况下的系统行为
- 自动化执行:集成到CI/CD流程中的自动化测试
通过系统化的集成测试,可以在早期发现组件间的协作问题,确保系统的整体稳定性。