测试 #

一、测试概述 #

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