2
0

integration-testing-best-practices.md 8.5 KB

集成测试最佳实践

概述

本文档提供了在项目中编写和维护集成测试的最佳实践指南。集成测试用于验证多个组件或服务之间的协作是否正确。

测试类型区分

单元测试 vs 集成测试

测试类型 测试范围 使用场景
单元测试 单个函数、方法、类 验证独立逻辑的正确性
集成测试 多个组件、服务协作 验证系统各部分集成是否正确
端到端测试 完整用户流程 验证从用户界面到后端的完整流程

API集成测试模式

基本测试结构

describe('API Endpoint Integration', () => {
  let app: Hono;
  let apiClient: ApiClient;

  beforeEach(async () => {
    // 设置测试环境
    vi.clearAllMocks();

    // 创建测试应用
    app = new Hono();

    // 注册路由
    const routes = await import('./routes');
    routes.default(app);

    // 创建API客户端
    apiClient = createApiClient(app);
  });

  it('should return correct response', async () => {
    // 设置mock
    const mockService = require('./service').default;
    mockService.doSomething.mockResolvedValue({ data: 'test' });

    // 执行请求
    const response = await apiClient.get('/endpoint');

    // 验证结果
    expect(response.status).toBe(200);
    expect(response.data).toEqual({ data: 'test' });
    expect(mockService.doSomething).toHaveBeenCalled();
  });
});

认证和授权测试

describe('Authentication', () => {
  it('should require authentication', async () => {
    const response = await apiClient.get('/protected', {}, { authToken: undefined });
    expect(response.status).toBe(401);
  });

  it('should require specific roles', async () => {
    // Mock用户具有不同角色
    const response = await apiClient.get('/admin-only');
    expect(response.status).toBe(403);
  });
});

错误处理测试

describe('Error Handling', () => {
  it('should handle service errors gracefully', async () => {
    const mockService = require('./service').default;
    mockService.doSomething.mockRejectedValue(new Error('Service unavailable'));

    const response = await apiClient.get('/endpoint');

    expect(response.status).toBe(500);
    expect(response.data).toMatchObject({
      code: 500,
      message: 'Service unavailable'
    });
  });

  it('should handle validation errors', async () => {
    const response = await apiClient.post('/endpoint', { invalid: 'data' });

    expect(response.status).toBe(400);
    expect(response.data).toHaveProperty('error');
  });
});

React组件集成测试模式

组件渲染测试

describe('Component Integration', () => {
  it('should render with correct props', () => {
    render(
      <TestWrapper>
        <MyComponent title="Test" onClick={mockHandler} />
      </TestWrapper>
    );

    expect(screen.getByText('Test')).toBeInTheDocument();
    expect(screen.getByRole('button')).toBeEnabled();
  });

  it('should handle user interactions', async () => {
    const user = userEvent.setup();
    const handleClick = vi.fn();

    render(
      <TestWrapper>
        <MyComponent onClick={handleClick} />
      </TestWrapper>
    );

    await user.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

路由和导航测试

describe('Routing', () => {
  it('should navigate to correct route', async () => {
    renderWithRouter(<App />, { route: '/login' });

    expect(screen.getByLabelText('Email')).toBeInTheDocument();
    expect(screen.getByLabelText('Password')).toBeInTheDocument();
  });

  it('should handle protected routes', () => {
    renderWithRouter(<App />, { route: '/admin' });

    // 未认证用户应该被重定向
    expect(screen.getByText('Redirecting to login...')).toBeInTheDocument();
  });
});

状态管理测试

describe('State Management', () => {
  it('should update UI based on state changes', async () => {
    render(
      <TestWrapper>
        <UserProfile />
      </TestWrapper>
    );

    // 初始状态
    expect(screen.getByText('Loading...')).toBeInTheDocument();

    // 等待数据加载
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
    });
  });
});

Mock策略

数据库Mock

// 使用内存数据库
const testDataSource = new DataSource({
  type: 'better-sqlite3',
  database: ':memory:',
  synchronize: true,
  entities: [User, Role]
});

// 或者使用mock repository
const mockRepo = {
  find: vi.fn().mockResolvedValue([]),
  findOne: vi.fn().mockResolvedValue(null),
  save: vi.fn().mockResolvedValue({ id: 1 })
};

外部服务Mock

// 使用MSW mock HTTP请求
import { setupServer } from 'msw/node';
import { rest } from 'msw';

const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(ctx.json([{ id: 1, name: 'Test User' }]));
  })
);

// 或者使用vi.mock
vi.mock('../external-service', () => ({
  fetchData: vi.fn().mockResolvedValue({ data: 'test' })
}));

认证Mock

// Mock JWT验证
vi.mock('../auth/middleware', () => ({
  authMiddleware: vi.fn().mockImplementation((c, next) => {
    c.set('user', { id: 1, username: 'testuser' });
    return next();
  })
}));

测试数据管理

测试数据工厂

export function createTestUser(overrides: Partial<User> = {}): User {
  return {
    id: 1,
    username: 'testuser',
    email: 'test@example.com',
    password: 'hashed_password',
    createdAt: new Date(),
    updatedAt: new Date(),
    ...overrides
  };
}

// 使用工厂创建测试数据
const adminUser = createTestUser({ username: 'admin', roles: ['admin'] });
const inactiveUser = createTestUser({ status: 'inactive' });

测试数据清理

afterEach(async () => {
  // 清理测试数据
  await testDataSource.getRepository(User).clear();
  await testDataSource.getRepository(Role).clear();
});

afterAll(async () => {
  // 关闭数据库连接
  await testDataSource.destroy();
});

性能优化

测试并行化

// 在vitest.config.ts中配置
export default defineConfig({
  test: {
    maxThreads: 4,
    minThreads: 2,
    fileParallelism: true
  }
});

测试数据隔离

// 使用事务确保测试隔离
describe('User Tests', () => {
  let dataSource: DataSource;
  let queryRunner: QueryRunner;

  beforeEach(async () => {
    dataSource = await createTestDataSource();
    queryRunner = dataSource.createQueryRunner();
    await queryRunner.startTransaction();
  });

  afterEach(async () => {
    await queryRunner.rollbackTransaction();
    await queryRunner.release();
  });

  afterAll(async () => {
    await dataSource.destroy();
  });
});

常见陷阱和解决方案

1. 测试相互干扰

问题: 一个测试修改了全局状态,影响其他测试 解决方案: 使用beforeEachafterEach清理状态

2. 测试执行缓慢

问题: 集成测试执行时间过长 解决方案: 使用内存数据库,mock外部服务

3. 测试不稳定

问题: 测试有时通过,有时失败 解决方案: 避免依赖时间、随机数,使用固定测试数据

4. Mock过于复杂

问题: Mock代码比实际代码还复杂 解决方案: 使用工具函数简化mock创建

代码质量检查

测试覆盖率目标

  • 行覆盖率: > 70%
  • 分支覆盖率: > 70%
  • 函数覆盖率: > 70%
  • 语句覆盖率: > 70%

代码审查清单

  • 测试名称清晰描述预期行为
  • 使用恰当的断言
  • 包含错误场景测试
  • 测试数据准备充分
  • 测试清理逻辑完整
  • 没有重复的测试代码
  • 遵循AAA模式(Arrange-Act-Assert)

工具和配置

推荐工具

  • 测试框架: Vitest
  • API测试: Supertest + 自定义API客户端
  • 组件测试: Testing Library + Happy DOM
  • HTTP Mock: MSW (Mock Service Worker)
  • 数据库: SQLite内存数据库

配置文件示例

// vitest.config.ts
export default defineConfig({
  test: {
    environment: 'node',
    include: ['**/__integration_tests__/**/*.test.ts'],
    setupFiles: ['./src/test/setup.ts'],
    globals: true
  }
});

总结

集成测试是确保系统各部分正确协作的关键。遵循这些最佳实践可以编写出可靠、可维护的集成测试。记住测试的目标是增加信心,而不是追求100%的覆盖率。专注于测试关键业务逻辑和集成点。