本文档提供了项目集成测试的最佳实践、模式和指南,帮助开发团队编写高质量、可维护的集成测试。
src/**/__tests__/**/*.test.{ts,tsx}src/**/__integration_tests__/**/*.integration.test.{ts,tsx}tests/e2e/**/*.test.{ts,tsx}describe('API Integration Tests', () => {
let app: Hono;
let apiClient: ApiClient;
beforeEach(async () => {
// 设置测试环境
app = createTestApp();
apiClient = createApiClient(app);
});
afterEach(() => {
// 清理资源
});
});
it('应该返回正确的状态码和数据', async () => {
const response = await apiClient.get('/api/endpoint');
expect(response.status).toBe(200);
expect(response.data).toMatchObject(expectedData);
});
it('应该处理错误情况', async () => {
const response = await apiClient.get('/api/invalid-endpoint');
expect(response.status).toBe(404);
expect(response.data).toHaveProperty('error');
});
it('需要认证的端点应该验证令牌', async () => {
apiClient.clearAuthToken();
const response = await apiClient.get('/api/protected');
expect(response.status).toBe(401);
});
it('有效令牌应该允许访问', async () => {
apiClient.setAuthToken('valid-token');
const response = await apiClient.get('/api/protected');
expect(response.status).toBe(200);
});
it('应该渲染所有子组件', () => {
render(
<TestWrapper>
<ComplexComponent />
</TestWrapper>
);
expect(screen.getByText('Expected Text')).toBeInTheDocument();
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('应该处理用户输入和提交', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(
<TestWrapper>
<FormComponent onSubmit={onSubmit} />
</TestWrapper>
);
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.click(screen.getByRole('button', { name: 'Submit' }));
expect(onSubmit).toHaveBeenCalledWith('test@example.com');
});
it('应该正确处理导航', async () => {
const user = userEvent.setup();
render(
<TestQueryProvider>
<TestRouter initialPath="/home">
<App />
</TestRouter>
</TestQueryProvider>
);
await user.click(screen.getByText('Go to Settings'));
expect(screen.getByText('Settings Page')).toBeInTheDocument();
});
// 使用内存数据库
const testDataSource = new DataSource({
type: 'better-sqlite3',
database: ':memory:',
entities: [User, Role],
synchronize: true,
});
// 使用事务回滚确保测试隔离
beforeEach(async () => {
await testDataSource.initialize();
});
afterEach(async () => {
await testDataSource.destroy();
});
// 使用 vi.mock()
vi.mock('../services/external-service', () => ({
ExternalService: {
fetchData: vi.fn().mockResolvedValue(mockData),
sendData: vi.fn().mockResolvedValue({ success: true }),
}
}));
// 或者使用自定义mock工具
import { ServiceMocks } from '../__test_utils__/service-mocks';
beforeEach(() => {
ServiceMocks.setupForSuccess();
});
// 使用 MSW (Mock Service Worker)
import { setupServer } from 'msw/node';
import { rest } from 'msw';
const server = setupServer(
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.json({ users: [] }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
export function createTestUser(overrides = {}): User {
return {
id: 1,
username: 'testuser',
email: 'test@example.com',
createdAt: new Date(),
...overrides
};
}
// 使用
const adminUser = createTestUser({ role: 'admin' });
const inactiveUser = createTestUser({ active: false });
// 策略1: 事务回滚 (推荐)
describe('User API', () => {
let dataSource: DataSource;
beforeEach(async () => {
dataSource = await TestDatabase.initialize();
await dataSource.startTransaction();
});
afterEach(async () => {
await dataSource.rollbackTransaction();
await dataSource.destroy();
});
});
// 策略2: 清理数据库
afterEach(async () => {
await dataSource.getRepository(User).clear();
await dataSource.getRepository(Role).clear();
});
// 共享测试资源
let sharedDataSource: DataSource;
beforeAll(async () => {
sharedDataSource = await TestDatabase.initialize();
});
afterAll(async () => {
await sharedDataSource.destroy();
});
// 使用并行测试执行
// 在 vitest.config.ts 中配置
export default defineConfig({
test: {
maxThreads: 4,
minThreads: 2,
// ...
}
});
// 避免在每个测试中重新初始化
beforeAll(async () => {
// 一次性初始化
});
// 使用mock代替真实操作
vi.mock('heavy-operation-module');
// 添加调试输出
test('debug test', async () => {
console.log('Starting test...');
// 测试代码
console.log('Test completed');
});
// 使用 --test-timeout 参数增加超时时间
// vitest --test-timeout=30000
// 记录所有API请求
beforeEach(() => {
vi.spyOn(global, 'fetch').mockImplementation(async (input, init) => {
console.log('API Request:', input, init);
return mockResponse;
});
});
解决: 使用 waitFor 和适当的断言
// 错误方式
expect(element).toBeInTheDocument(); // 可能尚未渲染
// 正确方式
await waitFor(() => {
expect(element).toBeInTheDocument();
});
解决: 确保 afterEach/afterAll 中清理所有资源
afterEach(async () => {
await cleanupTestResources();
vi.clearAllMocks();
vi.resetAllMocks();
});
# GitHub Actions 示例
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run test:integration
# 生成测试报告
- run: npm run test:integration -- --reporter=junit --outputFile=test-results.xml
- uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results.xml
// .eslintrc.js
module.exports = {
rules: {
'testing-library/await-async-utils': 'error',
'testing-library/no-await-sync-events': 'error',
'testing-library/no-debugging-utils': 'warn',
'testing-library/no-dom-import': 'error',
}
};
# 运行集成测试
npm run test:integration
# 运行特定测试文件
npm run test:integration -- src/server/__integration_tests__/users.integration.test.ts
# 调试模式
npm run test:integration -- --inspect-brk
# 生成覆盖率报告
npm run test:integration -- --coverage
最后更新: 2025-09-15 版本: 1.0