跳到主要内容

测试

本章节介绍 Express.js 应用的测试方法和工具。

测试工具

npm install -D jest supertest

Jest 配置

jest.config.js:

module.exports = {
testEnvironment: 'node',
coveragePathIgnorePatterns: ['/node_modules/'],
testMatch: ['**/*.test.js'],
setupFilesAfterEnv: ['./tests/setup.js']
};

package.json:

{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}

测试设置

tests/setup.js:

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();
});

afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
});

单元测试

测试工具函数

utils/calculator.js:

const add = (a, b) => a + b;
const subtract = (a, b) => a - b;

module.exports = { add, subtract };

utils/calculator.test.js:

const { add, subtract } = require('./calculator');

describe('Calculator', () => {
test('should add two numbers', () => {
expect(add(1, 2)).toBe(3);
});

test('should subtract two numbers', () => {
expect(subtract(5, 3)).toBe(2);
});
});

测试中间件

const authMiddleware = require('../middleware/auth');

describe('Auth Middleware', () => {
test('should return 401 if no token', () => {
const req = { headers: {} };
const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
const next = jest.fn();

authMiddleware(req, res, next);

expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ error: '未授权' });
expect(next).not.toHaveBeenCalled();
});

test('should call next with valid token', () => {
const req = {
headers: { authorization: 'Bearer valid-token' }
};
const res = {};
const next = jest.fn();

authMiddleware(req, res, next);

expect(next).toHaveBeenCalled();
});
});

集成测试

测试路由

tests/routes/users.test.js:

const request = require('supertest');
const app = require('../../src/app');
const User = require('../../src/models/User');

describe('Users API', () => {
describe('GET /api/users', () => {
test('should return all users', async () => {
await User.create([
{ username: 'user1', email: '[email protected]', password: 'password' },
{ username: 'user2', email: '[email protected]', password: 'password' }
]);

const res = await request(app).get('/api/users');

expect(res.status).toBe(200);
expect(res.body.users).toHaveLength(2);
});
});

describe('POST /api/users', () => {
test('should create a new user', async () => {
const userData = {
username: 'newuser',
email: '[email protected]',
password: 'password123'
};

const res = await request(app)
.post('/api/users')
.send(userData);

expect(res.status).toBe(201);
expect(res.body.username).toBe(userData.username);
});

test('should return 400 for invalid data', async () => {
const res = await request(app)
.post('/api/users')
.send({ username: '' });

expect(res.status).toBe(400);
});
});

describe('GET /api/users/:id', () => {
test('should return a user', async () => {
const user = await User.create({
username: 'testuser',
email: '[email protected]',
password: 'password'
});

const res = await request(app).get(`/api/users/${user._id}`);

expect(res.status).toBe(200);
expect(res.body.username).toBe('testuser');
});

test('should return 404 for non-existent user', async () => {
const fakeId = new mongoose.Types.ObjectId();
const res = await request(app).get(`/api/users/${fakeId}`);

expect(res.status).toBe(404);
});
});
});

测试认证路由

describe('Auth API', () => {
describe('POST /api/auth/register', () => {
test('should register a new user', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({
username: 'testuser',
email: '[email protected]',
password: 'password123'
});

expect(res.status).toBe(201);
expect(res.body.token).toBeDefined();
});

test('should not register duplicate email', async () => {
await User.create({
username: 'existing',
email: '[email protected]',
password: 'password'
});

const res = await request(app)
.post('/api/auth/register')
.send({
username: 'newuser',
email: '[email protected]',
password: 'password123'
});

expect(res.status).toBe(400);
});
});

describe('POST /api/auth/login', () => {
test('should login with valid credentials', async () => {
await User.create({
username: 'testuser',
email: '[email protected]',
password: await bcrypt.hash('password123', 10)
});

const res = await request(app)
.post('/api/auth/login')
.send({
email: '[email protected]',
password: 'password123'
});

expect(res.status).toBe(200);
expect(res.body.token).toBeDefined();
});
});
});

测试覆盖率

运行覆盖率报告:

npm run test:coverage

覆盖率报告示例:

File          | % Stmts | % Branch | % Funcs | % Lines |
--------------|---------|----------|---------|---------|
All files | 85.71 | 75.00 | 90.00 | 85.71 |
controllers/ | 88.89 | 80.00 | 85.71 | 88.89 |
middleware/ | 80.00 | 66.67 | 100.00 | 80.00 |
routes/ | 85.00 | 75.00 | 87.50 | 85.00 |

测试最佳实践

  1. 隔离测试:每个测试应该独立运行
  2. 使用测试数据库:不要使用生产数据库
  3. 清理数据:每个测试后清理数据
  4. 描述性命名:测试名称应清晰描述测试内容
  5. 测试边界情况:不仅测试正常情况,还要测试错误情况