测试 #
一、测试概述 #
1.1 测试类型 #
| 类型 | 说明 |
|---|---|
| 单元测试 | 测试单个函数/模块 |
| 集成测试 | 测试模块间交互 |
| 端到端测试 | 测试完整流程 |
1.2 测试工具 #
| 工具 | 用途 |
|---|---|
| Jest | 测试框架 |
| Supertest | HTTP测试 |
| Mocha | 测试框架 |
| Chai | 断言库 |
二、Jest配置 #
2.1 安装 #
bash
npm install jest supertest --save-dev
2.2 配置 #
jest.config.js:
javascript
module.exports = {
testEnvironment: 'node',
coveragePathIgnorePatterns: ['/node_modules/'],
testMatch: ['**/*.test.js'],
setupFilesAfterEnv: ['./tests/setup.js']
};
package.json:
json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
2.3 测试环境 #
tests/setup.js:
javascript
process.env.NODE_ENV = 'test';
process.env.DATABASE_URL = 'mongodb://localhost:27017/test_db';
const mongoose = require('mongoose');
beforeAll(async () => {
await mongoose.connect(process.env.DATABASE_URL);
});
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
});
三、单元测试 #
3.1 测试工具函数 #
utils/helpers.js:
javascript
const formatDate = (date) => {
return new Date(date).toLocaleDateString('zh-CN');
};
const truncate = (str, length) => {
if (str.length <= length) return str;
return str.substring(0, length) + '...';
};
module.exports = { formatDate, truncate };
tests/helpers.test.js:
javascript
const { formatDate, truncate } = require('../utils/helpers');
describe('formatDate', () => {
it('should format date correctly', () => {
const date = '2024-01-15';
const result = formatDate(date);
expect(result).toBe('2024/1/15');
});
});
describe('truncate', () => {
it('should truncate long string', () => {
const result = truncate('Hello World', 5);
expect(result).toBe('Hello...');
});
it('should not truncate short string', () => {
const result = truncate('Hello', 10);
expect(result).toBe('Hello');
});
});
3.2 测试模型 #
models/User.js:
javascript
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true }
});
userSchema.methods.comparePassword = async function(password) {
return bcrypt.compare(password, this.password);
};
module.exports = mongoose.model('User', userSchema);
tests/models/User.test.js:
javascript
const User = require('../../models/User');
describe('User Model', () => {
it('should create user successfully', async () => {
const userData = {
name: '张三',
email: 'test@example.com',
password: 'password123'
};
const user = new User(userData);
await user.save();
expect(user._id).toBeDefined();
expect(user.name).toBe(userData.name);
expect(user.email).toBe(userData.email);
});
it('should fail without required fields', async () => {
const user = new User({});
await expect(user.save()).rejects.toThrow();
});
});
四、API测试 #
4.1 测试应用 #
app.js:
javascript
const express = require('express');
const app = express();
app.use(express.json());
app.get('/api/users', async (req, res) => {
const users = await User.find();
res.json(users);
});
app.post('/api/users', async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
});
module.exports = app;
4.2 测试路由 #
tests/routes/users.test.js:
javascript
const request = require('supertest');
const app = require('../../app');
const User = require('../../models/User');
describe('Users API', () => {
describe('GET /api/users', () => {
it('should return all users', async () => {
await User.create([
{ name: '张三', email: 'zhangsan@example.com', password: '123456' },
{ name: '李四', email: 'lisi@example.com', password: '123456' }
]);
const res = await request(app).get('/api/users');
expect(res.status).toBe(200);
expect(res.body.length).toBe(2);
});
});
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
name: '王五',
email: 'wangwu@example.com',
password: '123456'
};
const res = await request(app)
.post('/api/users')
.send(userData);
expect(res.status).toBe(201);
expect(res.body.name).toBe(userData.name);
expect(res.body.email).toBe(userData.email);
});
it('should fail with invalid data', async () => {
const res = await request(app)
.post('/api/users')
.send({});
expect(res.status).toBe(400);
});
});
});
4.3 认证测试 #
tests/routes/auth.test.js:
javascript
const request = require('supertest');
const app = require('../../app');
const User = require('../../models/User');
describe('Auth API', () => {
describe('POST /api/auth/register', () => {
it('should register a new user', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({
name: '张三',
email: 'zhangsan@example.com',
password: 'password123'
});
expect(res.status).toBe(201);
expect(res.body.token).toBeDefined();
});
it('should fail with existing email', async () => {
await User.create({
name: '张三',
email: 'zhangsan@example.com',
password: 'password123'
});
const res = await request(app)
.post('/api/auth/register')
.send({
name: '李四',
email: 'zhangsan@example.com',
password: 'password123'
});
expect(res.status).toBe(400);
});
});
describe('POST /api/auth/login', () => {
beforeEach(async () => {
await request(app)
.post('/api/auth/register')
.send({
name: '张三',
email: 'zhangsan@example.com',
password: 'password123'
});
});
it('should login with correct credentials', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'zhangsan@example.com',
password: 'password123'
});
expect(res.status).toBe(200);
expect(res.body.token).toBeDefined();
});
it('should fail with wrong password', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'zhangsan@example.com',
password: 'wrongpassword'
});
expect(res.status).toBe(401);
});
});
});
五、Mock和Stub #
5.1 Mock函数 #
javascript
const sendEmail = require('../utils/email');
jest.mock('../utils/email');
describe('User Service', () => {
it('should send welcome email', async () => {
sendEmail.mockResolvedValue(true);
await userService.create({
name: '张三',
email: 'test@example.com',
password: '123456'
});
expect(sendEmail).toHaveBeenCalledWith(
'test@example.com',
'欢迎注册',
expect.any(String)
);
});
});
5.2 Mock数据库 #
javascript
jest.mock('../models/User');
describe('User Service', () => {
it('should find user by email', async () => {
const mockUser = { name: '张三', email: 'test@example.com' };
User.findOne.mockResolvedValue(mockUser);
const user = await userService.findByEmail('test@example.com');
expect(user).toEqual(mockUser);
expect(User.findOne).toHaveBeenCalledWith({ email: 'test@example.com' });
});
});
六、测试覆盖率 #
6.1 运行覆盖率 #
bash
npm run test:coverage
6.2 覆盖率报告 #
text
----------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
All files | 85.71 | 75.00 | 90.00 | 85.71 |
app.js | 90.00 | 80.00 | 100.00 | 90.00 |
utils/ | 80.00 | 70.00 | 80.00 | 80.00 |
----------|---------|----------|---------|---------|
七、持续集成 #
7.1 GitHub Actions #
.github/workflows/test.yml:
yaml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
mongodb:
image: mongo:latest
ports:
- 27017:27017
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm test
env:
DATABASE_URL: mongodb://localhost:27017/test
八、总结 #
测试要点:
| 类型 | 工具 | 说明 |
|---|---|---|
| 单元测试 | Jest | 测试函数/模块 |
| API测试 | Supertest | 测试HTTP接口 |
| Mock | jest.mock() | 模拟依赖 |
| 覆盖率 | Jest | 代码覆盖报告 |
下一步,让我们学习实战案例!
最后更新:2026-03-28