您现在的位置是:网站首页 > 单元测试与集成测试文章详情
单元测试与集成测试
陈川
【
Node.js
】
8766人已围观
8875字
单元测试与集成测试的基本概念
单元测试是针对软件中最小的可测试单元进行的测试,通常是函数或方法。集成测试则是将多个单元组合在一起进行测试,验证它们之间的交互是否正确。在Express框架中,单元测试可能针对单个路由处理函数,而集成测试则可能测试整个路由与中间件的配合。
// 单元测试示例:测试一个简单的工具函数
function add(a, b) {
return a + b;
}
// 集成测试示例:测试Express路由
const express = require('express');
const app = express();
app.get('/sum', (req, res) => {
const result = add(Number(req.query.a), Number(req.query.b));
res.json({ result });
});
Express中的单元测试实践
在Express应用中,单元测试通常关注以下几个方面:
- 路由处理函数的独立测试
- 中间件函数的测试
- 工具类和辅助函数的测试
使用Jest进行单元测试的典型示例:
// userController.js
exports.getUser = (req, res) => {
const user = { id: req.params.id, name: 'Test User' };
res.status(200).json(user);
};
// userController.test.js
const { getUser } = require('./userController');
const mockRequest = (params) => ({ params });
const mockResponse = () => {
const res = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
return res;
};
test('getUser should return user data', () => {
const req = mockRequest({ id: '123' });
const res = mockResponse();
getUser(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
id: '123',
name: 'Test User'
});
});
Express中的集成测试方法
集成测试需要启动Express应用并模拟真实请求。Supertest是专门为HTTP服务器测试设计的库:
const request = require('supertest');
const app = require('../app');
describe('GET /api/users', () => {
it('should return list of users', async () => {
const response = await request(app)
.get('/api/users')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveLength(3);
expect(response.body[0]).toHaveProperty('id');
expect(response.body[0]).toHaveProperty('name');
});
it('should create new user', async () => {
const newUser = { name: 'New User' };
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
name: 'New User'
});
});
});
测试数据库交互
真实应用通常需要测试数据库操作,可以使用内存数据库或模拟:
// 使用Jest模拟Mongoose模型
jest.mock('../models/User');
const User = require('../models/User');
const { getUsers } = require('./userController');
test('getUsers should fetch from database', async () => {
const mockUsers = [{ name: 'User1' }, { name: 'User2' }];
User.find.mockResolvedValue(mockUsers);
const req = {};
const res = mockResponse();
await getUsers(req, res);
expect(User.find).toHaveBeenCalled();
expect(res.json).toHaveBeenCalledWith(mockUsers);
});
// 集成测试中使用真实数据库连接
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
test('should save user to database', async () => {
const User = require('../models/User');
const user = new User({ name: 'Test' });
const savedUser = await user.save();
expect(savedUser._id).toBeDefined();
expect(savedUser.name).toBe('Test');
});
测试中间件
中间件测试需要特别注意next函数的调用:
// authMiddleware.js
module.exports = (req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
};
// authMiddleware.test.js
const authMiddleware = require('./authMiddleware');
test('should block unauthorized requests', () => {
const req = { headers: {} };
const res = mockResponse();
authMiddleware(req, res, jest.fn());
expect(res.status).toHaveBeenCalledWith(401);
});
test('should allow authorized requests', () => {
const req = { headers: { authorization: 'Bearer token' } };
const res = mockResponse();
const next = jest.fn();
authMiddleware(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
测试错误处理
验证应用对错误情况的处理:
// errorHandler.js
module.exports = (err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
};
// 测试路由抛出错误的情况
app.get('/error', (req, res, next) => {
next(new Error('Test error'));
});
// 在测试中
it('should handle errors properly', async () => {
const response = await request(app)
.get('/error')
.expect(500);
expect(response.body).toEqual({
error: 'Something went wrong!'
});
});
测试文件上传
测试文件上传等复杂请求:
const path = require('path');
const fs = require('fs');
it('should upload file', async () => {
const filePath = path.join(__dirname, 'test-file.txt');
fs.writeFileSync(filePath, 'test content');
const response = await request(app)
.post('/upload')
.attach('file', filePath)
.expect(200);
expect(response.body).toHaveProperty('filename');
fs.unlinkSync(filePath);
});
测试认证和会话
测试需要认证的路由:
// 先登录获取token
let authToken;
beforeAll(async () => {
const loginRes = await request(app)
.post('/login')
.send({ username: 'test', password: 'test' });
authToken = loginRes.body.token;
});
it('should access protected route with valid token', async () => {
const response = await request(app)
.get('/protected')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body).toHaveProperty('secretData');
});
测试性能与负载
虽然不是传统测试,但可以验证中间件的性能影响:
const slowMiddleware = (req, res, next) => {
// 模拟耗时操作
for (let i = 0; i < 1000000; i++) {}
next();
};
test('should not add significant latency', () => {
const start = Date.now();
const req = {};
const res = {};
const next = jest.fn();
slowMiddleware(req, res, next);
const duration = Date.now() - start;
expect(duration).toBeLessThan(100); // 毫秒
expect(next).toHaveBeenCalled();
});
测试配置和环境变量
验证不同环境下的配置:
// config.js
module.exports = {
dbUrl: process.env.DB_URL || 'mongodb://localhost:27017/test',
port: process.env.PORT || 3000
};
// config.test.js
process.env.DB_URL = 'mongodb://memory';
process.env.PORT = '0';
const config = require('./config');
test('should use test environment variables', () => {
expect(config.dbUrl).toBe('mongodb://memory');
expect(config.port).toBe('0');
});
测试WebSocket集成
如果应用使用WebSocket:
const WebSocket = require('ws');
const { createServer } = require('http');
test('should handle WebSocket connections', (done) => {
const server = createServer();
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws) => {
ws.on('message', (message) => {
ws.send(`Echo: ${message}`);
});
});
server.listen(() => {
const { port } = server.address();
const ws = new WebSocket(`ws://localhost:${port}`);
ws.on('open', () => {
ws.send('test');
});
ws.on('message', (message) => {
expect(message).toBe('Echo: test');
ws.close();
server.close();
done();
});
});
});
测试定时任务
对于定时执行的代码:
// taskScheduler.js
let executionCount = 0;
module.exports = {
run: () => { executionCount++ },
getCount: () => executionCount
};
// 使用Jest的定时器模拟
jest.useFakeTimers();
test('should run task periodically', () => {
const { run, getCount } = require('./taskScheduler');
setInterval(run, 1000);
jest.advanceTimersByTime(3000);
expect(getCount()).toBe(3);
});
测试第三方API调用
模拟外部API请求:
// weatherService.js
const axios = require('axios');
module.exports = {
getWeather: async (city) => {
const response = await axios.get(`https://api.weather.com/${city}`);
return response.data;
}
};
// 测试中使用Jest模拟axios
jest.mock('axios');
const axios = require('axios');
const { getWeather } = require('./weatherService');
test('should return weather data', async () => {
const mockData = { temp: 25, condition: 'Sunny' };
axios.get.mockResolvedValue({ data: mockData });
const weather = await getWeather('london');
expect(axios.get).toHaveBeenCalledWith('https://api.weather.com/london');
expect(weather).toEqual(mockData);
});
测试覆盖率与持续集成
配置Jest收集覆盖率:
// package.json
{
"jest": {
"collectCoverage": true,
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
在CI管道中运行测试:
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm ci
- run: npm test
- run: npm run test:coverage
上一篇: 日志系统的集成与配置
下一篇: API文档生成与维护