测试 #

一、测试概述 #

1.1 测试类型 #

类型 说明
单元测试 测试单个函数或模块
集成测试 测试多个组件协作
端到端测试 测试完整用户流程

1.2 测试工具 #

bash
npm install --save-dev jest

二、项目配置 #

2.1 package.json #

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

2.2 jest.config.js #

javascript
module.exports = {
    testEnvironment: 'node',
    testMatch: ['**/*.test.js'],
    collectCoverageFrom: [
        'src/**/*.js',
        '!src/**/*.test.js'
    ],
    coverageDirectory: 'coverage',
    coverageReporters: ['text', 'lcov']
};

三、服务器测试 #

3.1 创建测试服务器 #

javascript
const Hapi = require('@hapi/hapi');

const createServer = async () => {
    const server = Hapi.server({
        host: 'localhost',
        port: 3000
    });
    
    server.route([
        {
            method: 'GET',
            path: '/',
            handler: (request, h) => {
                return { message: 'Hello World' };
            }
        },
        {
            method: 'GET',
            path: '/users/{id}',
            handler: (request, h) => {
                return { id: request.params.id };
            }
        }
    ]);
    
    await server.initialize();
    return server;
};

module.exports = createServer;

3.2 测试服务器 #

javascript
const createServer = require('./server');

describe('Server', () => {
    let server;
    
    beforeEach(async () => {
        server = await createServer();
    });
    
    afterEach(async () => {
        await server.stop();
    });
    
    test('should start server', () => {
        expect(server).toBeDefined();
        expect(server.info.port).toBe(3000);
    });
    
    test('should return hello world', async () => {
        const response = await server.inject({
            method: 'GET',
            path: '/'
        });
        
        expect(response.statusCode).toBe(200);
        expect(response.result).toEqual({ message: 'Hello World' });
    });
});

四、路由测试 #

4.1 GET请求测试 #

javascript
describe('GET /users', () => {
    let server;
    
    beforeEach(async () => {
        server = await createServer();
    });
    
    afterEach(async () => {
        await server.stop();
    });
    
    test('should return all users', async () => {
        const response = await server.inject({
            method: 'GET',
            path: '/users'
        });
        
        expect(response.statusCode).toBe(200);
        expect(Array.isArray(response.result)).toBe(true);
    });
    
    test('should return user by id', async () => {
        const response = await server.inject({
            method: 'GET',
            path: '/users/1'
        });
        
        expect(response.statusCode).toBe(200);
        expect(response.result.id).toBe('1');
    });
    
    test('should return 404 for non-existent user', async () => {
        const response = await server.inject({
            method: 'GET',
            path: '/users/999'
        });
        
        expect(response.statusCode).toBe(404);
    });
});

4.2 POST请求测试 #

javascript
describe('POST /users', () => {
    let server;
    
    beforeEach(async () => {
        server = await createServer();
    });
    
    afterEach(async () => {
        await server.stop();
    });
    
    test('should create user', async () => {
        const response = await server.inject({
            method: 'POST',
            path: '/users',
            payload: {
                name: 'John Doe',
                email: 'john@example.com'
            }
        });
        
        expect(response.statusCode).toBe(201);
        expect(response.result.name).toBe('John Doe');
    });
    
    test('should reject invalid data', async () => {
        const response = await server.inject({
            method: 'POST',
            path: '/users',
            payload: {
                name: 'J'
            }
        });
        
        expect(response.statusCode).toBe(400);
    });
});

4.3 认证路由测试 #

javascript
describe('GET /profile', () => {
    let server;
    
    beforeEach(async () => {
        server = await createServer();
    });
    
    afterEach(async () => {
        await server.stop();
    });
    
    test('should return 401 without token', async () => {
        const response = await server.inject({
            method: 'GET',
            path: '/profile'
        });
        
        expect(response.statusCode).toBe(401);
    });
    
    test('should return profile with valid token', async () => {
        const token = generateTestToken();
        
        const response = await server.inject({
            method: 'GET',
            path: '/profile',
            headers: {
                Authorization: `Bearer ${token}`
            }
        });
        
        expect(response.statusCode).toBe(200);
        expect(response.result).toHaveProperty('id');
    });
});

五、处理函数测试 #

5.1 单元测试 #

javascript
const handlers = require('./handlers/users');

describe('User Handlers', () => {
    describe('getAll', () => {
        test('should return all users', async () => {
            const mockRequest = {
                query: { page: 1, limit: 10 }
            };
            
            const mockH = {
                response: jest.fn().mockReturnThis()
            };
            
            const result = await handlers.getAll(mockRequest, mockH);
            
            expect(result).toBeDefined();
        });
    });
    
    describe('getById', () => {
        test('should return user by id', async () => {
            const mockRequest = {
                params: { id: '1' }
            };
            
            const mockH = {
                response: jest.fn().mockReturnThis()
            };
            
            const result = await handlers.getById(mockRequest, mockH);
            
            expect(result).toBeDefined();
        });
    });
});

六、插件测试 #

6.1 测试插件 #

javascript
const myPlugin = require('./plugins/my-plugin');

describe('My Plugin', () => {
    let server;
    
    beforeEach(async () => {
        server = Hapi.server({ port: 3000 });
        await server.register(myPlugin);
    });
    
    afterEach(async () => {
        await server.stop();
    });
    
    test('should register successfully', () => {
        expect(server.registrations['my-plugin']).toBeDefined();
    });
    
    test('should add routes', async () => {
        const response = await server.inject({
            method: 'GET',
            path: '/plugin-route'
        });
        
        expect(response.statusCode).toBe(200);
    });
    
    test('should add methods', () => {
        expect(server.methods.myMethod).toBeDefined();
    });
});

七、验证测试 #

7.1 测试验证 #

javascript
const Joi = require('@hapi/joi');

describe('Validation', () => {
    const userSchema = Joi.object({
        name: Joi.string().min(2).required(),
        email: Joi.string().email().required()
    });
    
    test('should pass valid data', () => {
        const { error } = userSchema.validate({
            name: 'John',
            email: 'john@example.com'
        });
        
        expect(error).toBeUndefined();
    });
    
    test('should fail invalid email', () => {
        const { error } = userSchema.validate({
            name: 'John',
            email: 'invalid-email'
        });
        
        expect(error).toBeDefined();
        expect(error.details[0].type).toBe('string.email');
    });
    
    test('should fail missing name', () => {
        const { error } = userSchema.validate({
            email: 'john@example.com'
        });
        
        expect(error).toBeDefined();
        expect(error.details[0].type).toBe('any.required');
    });
});

八、Mock测试 #

8.1 Mock服务 #

javascript
jest.mock('./services/userService');

const UserService = require('./services/userService');
const handlers = require('./handlers/users');

describe('User Handlers with Mock', () => {
    beforeEach(() => {
        jest.clearAllMocks();
    });
    
    test('should call UserService.findAll', async () => {
        UserService.findAll.mockResolvedValue([
            { id: 1, name: 'John' }
        ]);
        
        const mockRequest = {
            query: { page: 1, limit: 10 }
        };
        
        const result = await handlers.getAll(mockRequest, {});
        
        expect(UserService.findAll).toHaveBeenCalledWith({ page: 1, limit: 10 });
        expect(result).toHaveLength(1);
    });
});

8.2 Mock数据库 #

javascript
const mockDb = {
    query: jest.fn()
};

jest.mock('./db', () => mockDb);

describe('Database Operations', () => {
    beforeEach(() => {
        jest.clearAllMocks();
    });
    
    test('should query database', async () => {
        mockDb.query.mockResolvedValue({
            rows: [{ id: 1, name: 'John' }]
        });
        
        const result = await getUserById(1);
        
        expect(mockDb.query).toHaveBeenCalledWith(
            'SELECT * FROM users WHERE id = $1',
            [1]
        );
        expect(result).toEqual({ id: 1, name: 'John' });
    });
});

九、测试覆盖率 #

9.1 运行覆盖率 #

bash
npm run test:coverage

9.2 覆盖率报告 #

javascript
module.exports = {
    coverageThreshold: {
        global: {
            branches: 80,
            functions: 80,
            lines: 80,
            statements: 80
        }
    }
};

十、完整测试示例 #

10.1 完整测试文件 #

javascript
const Hapi = require('@hapi/hapi');
const Joi = require('@hapi/joi');

const createServer = async () => {
    const server = Hapi.server({ port: 3000 });
    
    const users = [];
    
    server.route([
        {
            method: 'GET',
            path: '/users',
            options: {
                validate: {
                    query: Joi.object({
                        page: Joi.number().default(1),
                        limit: Joi.number().default(10)
                    })
                }
            },
            handler: (request, h) => {
                return {
                    users,
                    page: request.query.page,
                    limit: request.query.limit
                };
            }
        },
        {
            method: 'POST',
            path: '/users',
            options: {
                validate: {
                    payload: Joi.object({
                        name: Joi.string().min(2).required(),
                        email: Joi.string().email().required()
                    })
                }
            },
            handler: (request, h) => {
                const user = {
                    id: users.length + 1,
                    ...request.payload
                };
                users.push(user);
                return h.response(user).code(201);
            }
        }
    ]);
    
    await server.initialize();
    return server;
};

describe('User API', () => {
    let server;
    
    beforeEach(async () => {
        server = await createServer();
    });
    
    afterEach(async () => {
        await server.stop();
    });
    
    describe('GET /users', () => {
        test('should return empty array', async () => {
            const response = await server.inject({
                method: 'GET',
                path: '/users'
            });
            
            expect(response.statusCode).toBe(200);
            expect(response.result.users).toEqual([]);
        });
        
        test('should use default pagination', async () => {
            const response = await server.inject({
                method: 'GET',
                path: '/users'
            });
            
            expect(response.result.page).toBe(1);
            expect(response.result.limit).toBe(10);
        });
    });
    
    describe('POST /users', () => {
        test('should create user', async () => {
            const response = await server.inject({
                method: 'POST',
                path: '/users',
                payload: {
                    name: 'John Doe',
                    email: 'john@example.com'
                }
            });
            
            expect(response.statusCode).toBe(201);
            expect(response.result.id).toBe(1);
            expect(response.result.name).toBe('John Doe');
        });
        
        test('should reject invalid email', async () => {
            const response = await server.inject({
                method: 'POST',
                path: '/users',
                payload: {
                    name: 'John',
                    email: 'invalid'
                }
            });
            
            expect(response.statusCode).toBe(400);
        });
        
        test('should reject short name', async () => {
            const response = await server.inject({
                method: 'POST',
                path: '/users',
                payload: {
                    name: 'J',
                    email: 'john@example.com'
                }
            });
            
            expect(response.statusCode).toBe(400);
        });
    });
});

十一、总结 #

测试要点:

测试类型 工具
单元测试 Jest
集成测试 server.inject()
Mock jest.mock()
覆盖率 Jest Coverage

下一步,让我们学习实战案例!

最后更新:2026-03-28