Koa 测试是保证应用质量的重要环节。通过合理的测试策略和工具,可以确保应用的稳定性和可靠性。
1. 测试框架选择:
常用的 Koa 测试框架包括:
- Jest:Facebook 开发的测试框架,功能全面
- Mocha + Chai:灵活的测试框架组合
- Supertest:专门用于 HTTP 测试的库
2. 基本测试设置:
安装依赖:
bashnpm install --save-dev jest supertest @types/jest @types/supertest
Jest 配置:
javascript// jest.config.js module.exports = { testEnvironment: 'node', coverageDirectory: 'coverage', collectCoverageFrom: [ 'src/**/*.js', '!src/**/*.test.js' ], testMatch: [ '**/__tests__/**/*.js', '**/?(*.)+(spec|test).js' ] };
3. 基本路由测试:
javascriptconst request = require('supertest'); const app = require('../app'); describe('Basic routes', () => { test('GET / should return Hello Koa', async () => { const response = await request(app.callback()) .get('/') .expect(200); expect(response.text).toBe('Hello Koa'); }); test('GET /not-found should return 404', async () => { const response = await request(app.callback()) .get('/not-found') .expect(404); }); });
4. 中间件测试:
javascriptconst Koa = require('koa'); const loggerMiddleware = require('../middleware/logger'); describe('Logger middleware', () => { test('should log request information', async () => { const app = new Koa(); const logs = []; // Mock console.log const originalLog = console.log; console.log = (...args) => logs.push(args.join(' ')); app.use(loggerMiddleware); app.use(async (ctx) => { ctx.body = 'test'; }); await request(app.callback()).get('/test'); console.log = originalLog; expect(logs.length).toBeGreaterThan(0); expect(logs[0]).toContain('GET /test'); }); });
5. 认证中间件测试:
javascriptconst authMiddleware = require('../middleware/auth'); describe('Auth middleware', () => { test('should allow access with valid token', async () => { const ctx = { headers: { authorization: 'Bearer valid-token' }, state: {} }; const next = jest.fn(); await authMiddleware(ctx, next); expect(next).toHaveBeenCalled(); expect(ctx.state.user).toBeDefined(); }); test('should deny access without token', async () => { const ctx = { headers: {}, state: {}, throw: jest.fn() }; const next = jest.fn(); await authMiddleware(ctx, next); expect(next).not.toHaveBeenCalled(); expect(ctx.throw).toHaveBeenCalledWith(401, 'Unauthorized'); }); });
6. API 测试:
javascriptdescribe('User API', () => { test('POST /api/users should create user', async () => { const userData = { name: 'John Doe', email: 'john@example.com', password: 'password123' }; const response = await request(app.callback()) .post('/api/users') .send(userData) .expect(201); expect(response.body).toHaveProperty('id'); expect(response.body.name).toBe(userData.name); expect(response.body.email).toBe(userData.email); expect(response.body).not.toHaveProperty('password'); }); test('GET /api/users/:id should return user', async () => { const userId = 1; const response = await request(app.callback()) .get(`/api/users/${userId}`) .expect(200); expect(response.body).toHaveProperty('id', userId); expect(response.body).toHaveProperty('name'); expect(response.body).toHaveProperty('email'); }); test('PUT /api/users/:id should update user', async () => { const userId = 1; const updateData = { name: 'Updated Name' }; const response = await request(app.callback()) .put(`/api/users/${userId}`) .send(updateData) .expect(200); expect(response.body.name).toBe(updateData.name); }); test('DELETE /api/users/:id should delete user', async () => { const userId = 1; await request(app.callback()) .delete(`/api/users/${userId}`) .expect(204); }); });
7. 错误处理测试:
javascriptdescribe('Error handling', () => { test('should handle 404 errors', async () => { const response = await request(app.callback()) .get('/non-existent-route') .expect(404); expect(response.body).toHaveProperty('error'); expect(response.body).toHaveProperty('code', 'NOT_FOUND'); }); test('should handle validation errors', async () => { const invalidData = { name: '', email: 'invalid-email' }; const response = await request(app.callback()) .post('/api/users') .send(invalidData) .expect(400); expect(response.body).toHaveProperty('error'); expect(response.body).toHaveProperty('code', 'VALIDATION_ERROR'); }); test('should handle server errors', async () => { // Mock a database error jest.spyOn(User, 'create').mockRejectedValue(new Error('Database error')); const response = await request(app.callback()) .post('/api/users') .send({ name: 'Test', email: 'test@example.com' }) .expect(500); expect(response.body).toHaveProperty('error'); expect(response.body).toHaveProperty('code', 'INTERNAL_ERROR'); }); });
8. 集成测试:
javascriptdescribe('Integration tests', () => { let authToken; beforeAll(async () => { // Setup: create test user and get token const response = await request(app.callback()) .post('/api/auth/login') .send({ email: 'test@example.com', password: 'password123' }); authToken = response.body.token; }); afterAll(async () => { // Cleanup: delete test data await request(app.callback()) .delete('/api/test/cleanup'); }); test('should create and retrieve post', async () => { // Create post const createResponse = await request(app.callback()) .post('/api/posts') .set('Authorization', `Bearer ${authToken}`) .send({ title: 'Test Post', content: 'Test content' }) .expect(201); const postId = createResponse.body.id; // Retrieve post const getResponse = await request(app.callback()) .get(`/api/posts/${postId}`) .expect(200); expect(getResponse.body.title).toBe('Test Post'); expect(getResponse.body.content).toBe('Test content'); }); });
9. 性能测试:
javascriptdescribe('Performance tests', () => { test('should handle 1000 requests in reasonable time', async () => { const startTime = Date.now(); const requests = []; for (let i = 0; i < 1000; i++) { requests.push( request(app.callback()) .get('/api/users') .expect(200) ); } await Promise.all(requests); const duration = Date.now() - startTime; expect(duration).toBeLessThan(5000); // Should complete in < 5 seconds }); });
10. 测试最佳实践:
-
测试组织:
- 按功能模块组织测试文件
- 使用 describe 分组相关测试
- 使用有意义的测试名称
-
测试隔离:
- 每个测试应该独立运行
- 使用 beforeAll/afterAll 设置和清理
- 使用 beforeEach/afterEach 重置状态
-
Mock 和 Stub:
- Mock 外部依赖
- 使用测试数据库
- 避免测试真实网络请求
-
覆盖率:
- 设置覆盖率目标(通常 > 80%)
- 定期检查覆盖率报告
- 关注关键路径的覆盖率
-
持续集成:
- 在 CI/CD 流程中运行测试
- 测试失败时阻止部署
- 自动生成覆盖率报告
-
测试数据:
- 使用工厂模式创建测试数据
- 使用固定的测试数据
- 清理测试数据避免污染
javascript// 测试数据工厂 const userFactory = (overrides = {}) => ({ name: 'Test User', email: 'test@example.com', password: 'password123', ...overrides }); // 使用工厂 test('should create user', async () => { const userData = userFactory({ name: 'Custom Name' }); const response = await request(app.callback()) .post('/api/users') .send(userData) .expect(201); expect(response.body.name).toBe('Custom Name'); });