Procházet zdrojové kódy

集成测试环境现已完全配置完成,支持API端到端测试和React组件集成测试,并提供
了完整的最佳实践文档

yourname před 2 měsíci
rodič
revize
2e824d28aa

+ 292 - 236
docs/integration-testing-best-practices.md

@@ -1,364 +1,420 @@
-# 集成测试最佳实践
+# 集成测试最佳实践指南
 
 ## 概述
 
-本文档提供了在项目中编写和维护集成测试的最佳实践指南。集成测试用于验证多个组件或服务之间的协作是否正确
+本文档提供了项目集成测试的最佳实践、模式和指南,帮助开发团队编写高质量、可维护的集成测试
 
 ## 测试类型区分
 
-### 单元测试 vs 集成测试
+### 单元测试 (Unit Tests)
+- **范围**: 单个函数、类或组件
+- **目标**: 验证独立单元的 correctness
+- **位置**: `src/**/__tests__/**/*.test.{ts,tsx}`
 
-| 测试类型 | 测试范围 | 使用场景 |
-|---------|---------|---------|
-| **单元测试** | 单个函数、方法、类 | 验证独立逻辑的正确性 |
-| **集成测试** | 多个组件、服务协作 | 验证系统各部分集成是否正确 |
-| **端到端测试** | 完整用户流程 | 验证从用户界面到后端的完整流程 |
+### 集成测试 (Integration Tests)
+- **范围**: 多个组件/服务协作
+- **目标**: 验证模块间集成和交互
+- **位置**: `src/**/__integration_tests__/**/*.integration.test.{ts,tsx}`
 
-## API集成测试模式
+### E2E测试 (End-to-End Tests)
+- **范围**: 完整用户流程
+- **目标**: 验证端到端业务流程
+- **位置**: `tests/e2e/**/*.test.{ts,tsx}`
 
-### 基本测试结构
+## API 集成测试模式
 
+### 基本结构
 ```typescript
-describe('API Endpoint Integration', () => {
+describe('API Integration Tests', () => {
   let app: Hono;
   let apiClient: ApiClient;
 
   beforeEach(async () => {
     // 设置测试环境
-    vi.clearAllMocks();
-
-    // 创建测试应用
-    app = new Hono();
-
-    // 注册路由
-    const routes = await import('./routes');
-    routes.default(app);
-
-    // 创建API客户端
+    app = createTestApp();
     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();
+  afterEach(() => {
+    // 清理资源
   });
 });
 ```
 
-### 认证和授权测试
-
+### 请求测试模式
 ```typescript
-describe('Authentication', () => {
-  it('should require authentication', async () => {
-    const response = await apiClient.get('/protected', {}, { authToken: undefined });
-    expect(response.status).toBe(401);
-  });
+it('应该返回正确的状态码和数据', async () => {
+  const response = await apiClient.get('/api/endpoint');
 
-  it('should require specific roles', async () => {
-    // Mock用户具有不同角色
-    const response = await apiClient.get('/admin-only');
-    expect(response.status).toBe(403);
-  });
+  expect(response.status).toBe(200);
+  expect(response.data).toMatchObject(expectedData);
 });
-```
 
-### 错误处理测试
+it('应该处理错误情况', async () => {
+  const response = await apiClient.get('/api/invalid-endpoint');
 
-```typescript
-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');
-  });
+  expect(response.status).toBe(404);
+  expect(response.data).toHaveProperty('error');
 });
 ```
 
-## React组件集成测试模式
-
-### 组件渲染测试
-
+### 认证测试模式
 ```typescript
-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('需要认证的端点应该验证令牌', async () => {
+  apiClient.clearAuthToken();
 
-  it('should handle user interactions', async () => {
-    const user = userEvent.setup();
-    const handleClick = vi.fn();
+  const response = await apiClient.get('/api/protected');
+  expect(response.status).toBe(401);
+});
 
-    render(
-      <TestWrapper>
-        <MyComponent onClick={handleClick} />
-      </TestWrapper>
-    );
+it('有效令牌应该允许访问', async () => {
+  apiClient.setAuthToken('valid-token');
 
-    await user.click(screen.getByRole('button'));
-    expect(handleClick).toHaveBeenCalledTimes(1);
-  });
+  const response = await apiClient.get('/api/protected');
+  expect(response.status).toBe(200);
 });
 ```
 
-### 路由和导航测试
+## React 组件集成测试模式
 
+### 基本渲染测试
 ```typescript
-describe('Routing', () => {
-  it('should navigate to correct route', async () => {
-    renderWithRouter(<App />, { route: '/login' });
+it('应该渲染所有子组件', () => {
+  render(
+    <TestWrapper>
+      <ComplexComponent />
+    </TestWrapper>
+  );
+
+  expect(screen.getByText('Expected Text')).toBeInTheDocument();
+  expect(screen.getByRole('button')).toBeInTheDocument();
+});
+```
 
-    expect(screen.getByLabelText('Email')).toBeInTheDocument();
-    expect(screen.getByLabelText('Password')).toBeInTheDocument();
-  });
+### 用户交互测试
+```typescript
+it('应该处理用户输入和提交', async () => {
+  const user = userEvent.setup();
+  const onSubmit = vi.fn();
 
-  it('should handle protected routes', () => {
-    renderWithRouter(<App />, { route: '/admin' });
+  render(
+    <TestWrapper>
+      <FormComponent onSubmit={onSubmit} />
+    </TestWrapper>
+  );
 
-    // 未认证用户应该被重定向
-    expect(screen.getByText('Redirecting to login...')).toBeInTheDocument();
-  });
+  await user.type(screen.getByLabelText('Email'), 'test@example.com');
+  await user.click(screen.getByRole('button', { name: 'Submit' }));
+
+  expect(onSubmit).toHaveBeenCalledWith('test@example.com');
 });
 ```
 
-### 状态管理测试
-
+### 路由集成测试
 ```typescript
-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();
-    });
-  });
+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();
 });
 ```
 
-## Mock策略
-
-### 数据库Mock
+## Mock 策略
 
+### 数据库 Mock
 ```typescript
 // 使用内存数据库
 const testDataSource = new DataSource({
   type: 'better-sqlite3',
   database: ':memory:',
+  entities: [User, Role],
   synchronize: true,
-  entities: [User, Role]
 });
 
-// 或者使用mock repository
-const mockRepo = {
-  find: vi.fn().mockResolvedValue([]),
-  findOne: vi.fn().mockResolvedValue(null),
-  save: vi.fn().mockResolvedValue({ id: 1 })
-};
+// 使用事务回滚确保测试隔离
+beforeEach(async () => {
+  await testDataSource.initialize();
+});
+
+afterEach(async () => {
+  await testDataSource.destroy();
+});
 ```
 
-### 外部服务Mock
+### 服务 Mock
+```typescript
+// 使用 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();
+});
+```
 
+### HTTP 请求 Mock
 ```typescript
-// 使用MSW mock HTTP请求
+// 使用 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([{ id: 1, name: 'Test User' }]));
+    return res(ctx.json({ users: [] }));
   })
 );
 
-// 或者使用vi.mock
-vi.mock('../external-service', () => ({
-  fetchData: vi.fn().mockResolvedValue({ data: 'test' })
-}));
-```
-
-### 认证Mock
-
-```typescript
-// Mock JWT验证
-vi.mock('../auth/middleware', () => ({
-  authMiddleware: vi.fn().mockImplementation((c, next) => {
-    c.set('user', { id: 1, username: 'testuser' });
-    return next();
-  })
-}));
+beforeAll(() => server.listen());
+afterEach(() => server.resetHandlers());
+afterAll(() => server.close());
 ```
 
 ## 测试数据管理
 
 ### 测试数据工厂
-
 ```typescript
-export function createTestUser(overrides: Partial<User> = {}): User {
+export function createTestUser(overrides = {}): 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' });
+// 使用
+const adminUser = createTestUser({ role: 'admin' });
+const inactiveUser = createTestUser({ active: false });
 ```
 
-### 测试数据清理
-
+### 数据清理策略
 ```typescript
-afterEach(async () => {
-  // 清理测试数据
-  await testDataSource.getRepository(User).clear();
-  await testDataSource.getRepository(Role).clear();
+// 策略1: 事务回滚 (推荐)
+describe('User API', () => {
+  let dataSource: DataSource;
+
+  beforeEach(async () => {
+    dataSource = await TestDatabase.initialize();
+    await dataSource.startTransaction();
+  });
+
+  afterEach(async () => {
+    await dataSource.rollbackTransaction();
+    await dataSource.destroy();
+  });
 });
 
-afterAll(async () => {
-  // 关闭数据库连接
-  await testDataSource.destroy();
+// 策略2: 清理数据库
+afterEach(async () => {
+  await dataSource.getRepository(User).clear();
+  await dataSource.getRepository(Role).clear();
 });
 ```
 
 ## 性能优化
 
-### 测试并行化
-
+### 测试执行优化
 ```typescript
-// 在vitest.config.ts中配置
+// 共享测试资源
+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,
-    fileParallelism: true
+    // ...
   }
 });
 ```
 
-### 测试数据隔离
-
+### 减少不必要的操作
 ```typescript
-// 使用事务确保测试隔离
-describe('User Tests', () => {
-  let dataSource: DataSource;
-  let queryRunner: QueryRunner;
+// 避免在每个测试中重新初始化
+beforeAll(async () => {
+  // 一次性初始化
+});
 
-  beforeEach(async () => {
-    dataSource = await createTestDataSource();
-    queryRunner = dataSource.createQueryRunner();
-    await queryRunner.startTransaction();
-  });
+// 使用mock代替真实操作
+vi.mock('heavy-operation-module');
+```
 
-  afterEach(async () => {
-    await queryRunner.rollbackTransaction();
-    await queryRunner.release();
-  });
+## 调试技巧
 
-  afterAll(async () => {
-    await dataSource.destroy();
+### 测试调试
+```typescript
+// 添加调试输出
+test('debug test', async () => {
+  console.log('Starting test...');
+  // 测试代码
+  console.log('Test completed');
+});
+
+// 使用 --test-timeout 参数增加超时时间
+// vitest --test-timeout=30000
+```
+
+### 网络请求调试
+```typescript
+// 记录所有API请求
+beforeEach(() => {
+  vi.spyOn(global, 'fetch').mockImplementation(async (input, init) => {
+    console.log('API Request:', input, init);
+    return mockResponse;
   });
 });
 ```
 
-## 常见陷阱和解决方案
+## 常见问题解决
+
+### 测试不稳定 (Flaky Tests)
+- **原因**: 异步操作时序问题
+- **解决**: 使用 `waitFor` 和适当的断言
 
-### 1. 测试相互干扰
-**问题**: 一个测试修改了全局状态,影响其他测试
-**解决方案**: 使用`beforeEach`和`afterEach`清理状态
+```typescript
+// 错误方式
+expect(element).toBeInTheDocument(); // 可能尚未渲染
+
+// 正确方式
+await waitFor(() => {
+  expect(element).toBeInTheDocument();
+});
+```
 
-### 2. 测试执行缓慢
-**问题**: 集成测试执行时间过长
-**解决方案**: 使用内存数据库,mock外部服务
+### 内存泄漏
+- **原因**: 未正确清理资源
+- **解决**: 确保 afterEach/afterAll 中清理所有资源
 
-### 3. 测试不稳定
-**问题**: 测试有时通过,有时失败
-**解决方案**: 避免依赖时间、随机数,使用固定测试数据
+```typescript
+afterEach(async () => {
+  await cleanupTestResources();
+  vi.clearAllMocks();
+  vi.resetAllMocks();
+});
+```
 
-### 4. Mock过于复杂
-**问题**: Mock代码比实际代码还复杂
-**解决方案**: 使用工具函数简化mock创建
+### 测试执行缓慢
+- **原因**: 过多的真实数据库操作
+- **解决**: 使用内存数据库或更好的mock策略
+
+## CI/CD 集成
+
+### 测试配置
+```yaml
+# GitHub Actions 示例
+name: Integration Tests
+
+on: [push, pull_request]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+
+    services:
+      mysql:
+        image: mysql:8.0
+        env:
+          MYSQL_ROOT_PASSWORD: root
+          MYSQL_DATABASE: test_db
+        ports:
+          - 3306:3306
+        options: >-
+          --health-cmd="mysqladmin ping"
+          --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
+```
+
+### 测试报告
+```yaml
+# 生成测试报告
+- run: npm run test:integration -- --reporter=junit --outputFile=test-results.xml
+- uses: actions/upload-artifact@v4
+  with:
+    name: test-results
+    path: test-results.xml
+```
 
 ## 代码质量检查
 
-### 测试覆盖率目标
-- 行覆盖率: > 70%
-- 分支覆盖率: > 70%
-- 函数覆盖率: > 70%
-- 语句覆盖率: > 70%
+### ESLint 规则
+```javascript
+// .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',
+  }
+};
+```
 
-### 代码审查清单
-- [ ] 测试名称清晰描述预期行为
-- [ ] 使用恰当的断言
-- [ ] 包含错误场景测试
-- [ ] 测试数据准备充分
-- [ ] 测试清理逻辑完整
-- [ ] 没有重复的测试代码
-- [ ] 遵循AAA模式(Arrange-Act-Assert)
+### 测试覆盖率要求
+- **API 端点**: ≥ 80%
+- **关键业务逻辑**: ≥ 90%
+- **错误处理路径**: ≥ 70%
+- **整体覆盖率**: ≥ 75%
 
-## 工具和配置
+## 附录
 
-### 推荐工具
-- **测试框架**: Vitest
-- **API测试**: Supertest + 自定义API客户端
-- **组件测试**: Testing Library + Happy DOM
-- **HTTP Mock**: MSW (Mock Service Worker)
-- **数据库**: SQLite内存数据库
+### 有用命令
+```bash
+# 运行集成测试
+npm run test:integration
 
-### 配置文件示例
+# 运行特定测试文件
+npm run test:integration -- src/server/__integration_tests__/users.integration.test.ts
 
-```typescript
-// vitest.config.ts
-export default defineConfig({
-  test: {
-    environment: 'node',
-    include: ['**/__integration_tests__/**/*.test.ts'],
-    setupFiles: ['./src/test/setup.ts'],
-    globals: true
-  }
-});
+# 调试模式
+npm run test:integration -- --inspect-brk
+
+# 生成覆盖率报告
+npm run test:integration -- --coverage
 ```
 
-## 总结
+### 推荐工具
+- **Vitest**: 测试框架
+- **Testing Library**: React 测试工具
+- **MSW**: HTTP Mock 工具
+- **better-sqlite3**: 内存数据库
+- **Docker**: 测试数据库容器
+
+---
 
-集成测试是确保系统各部分正确协作的关键。遵循这些最佳实践可以编写出可靠、可维护的集成测试。记住测试的目标是增加信心,而不是追求100%的覆盖率。专注于测试关键业务逻辑和集成点。
+*最后更新: 2025-09-15*
+*版本: 1.0*

+ 1 - 0
package.json

@@ -104,6 +104,7 @@
     "@tailwindcss/vite": "^4.1.11",
     "@testing-library/react": "^16.3.0",
     "@testing-library/user-event": "^14.6.1",
+    "@testing-library/jest-dom": "^6.6.3",
     "@types/bcrypt": "^6.0.0",
     "@types/debug": "^4.1.12",
     "@types/jsonwebtoken": "^9.0.10",

+ 221 - 0
src/client/__integration_tests__/components/Form.integration.test.tsx

@@ -0,0 +1,221 @@
+import React from 'react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { TestWrapper } from '../../__test_utils__/test-render';
+import { TestRouter } from '../../__test_utils__/test-router';
+import { TestQueryProvider } from '../../__test_utils__/test-query';
+
+// 模拟的表单组件
+function LoginForm({ onSubmit }: { onSubmit: (data: any) => Promise<void> }) {
+  const [email, setEmail] = React.useState('');
+  const [password, setPassword] = React.useState('');
+  const [isSubmitting, setIsSubmitting] = React.useState(false);
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setIsSubmitting(true);
+    try {
+      await onSubmit({ email, password });
+    } catch (error) {
+      console.error('Login failed:', error);
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  return (
+    <form onSubmit={handleSubmit} data-testid="login-form">
+      <div>
+        <label htmlFor="email">Email:</label>
+        <input
+          id="email"
+          type="email"
+          value={email}
+          onChange={(e) => setEmail(e.target.value)}
+          required
+          data-testid="email-input"
+        />
+      </div>
+      <div>
+        <label htmlFor="password">Password:</label>
+        <input
+          id="password"
+          type="password"
+          value={password}
+          onChange={(e) => setPassword(e.target.value)}
+          required
+          data-testid="password-input"
+        />
+      </div>
+      <button
+        type="submit"
+        disabled={isSubmitting}
+        data-testid="submit-button"
+      >
+        {isSubmitting ? 'Logging in...' : 'Login'}
+      </button>
+    </form>
+  );
+}
+
+// 模拟的登录页面组件
+function LoginPage() {
+  const loginMutation = {
+    mutateAsync: vi.fn().mockResolvedValue({ success: true }),
+    isLoading: false,
+  };
+
+  const handleLogin = async (credentials: any) => {
+    return loginMutation.mutateAsync(credentials);
+  };
+
+  return (
+    <div>
+      <h1>Login</h1>
+      <LoginForm onSubmit={handleLogin} />
+    </div>
+  );
+}
+
+describe('Form Component Integration Tests', () => {
+  let user: ReturnType<typeof userEvent.setup>;
+  let loginMock: any;
+
+  beforeEach(() => {
+    user = userEvent.setup();
+    loginMock = vi.fn().mockResolvedValue({ success: true });
+  });
+
+  it('应该渲染表单并包含所有必要的字段', () => {
+    render(
+      <TestWrapper>
+        <LoginForm onSubmit={loginMock} />
+      </TestWrapper>
+    );
+
+    expect(screen.getByTestId('login-form')).toBeInTheDocument();
+    expect(screen.getByTestId('email-input')).toBeInTheDocument();
+    expect(screen.getByTestId('password-input')).toBeInTheDocument();
+    expect(screen.getByTestId('submit-button')).toBeInTheDocument();
+  });
+
+  it('应该允许用户输入邮箱和密码', async () => {
+    render(
+      <TestWrapper>
+        <LoginForm onSubmit={loginMock} />
+      </TestWrapper>
+    );
+
+    const emailInput = screen.getByTestId('email-input');
+    const passwordInput = screen.getByTestId('password-input');
+
+    await user.type(emailInput, 'test@example.com');
+    await user.type(passwordInput, 'password123');
+
+    expect(emailInput).toHaveValue('test@example.com');
+    expect(passwordInput).toHaveValue('password123');
+  });
+
+  it('应该在表单提交时调用onSubmit回调', async () => {
+    render(
+      <TestWrapper>
+        <LoginForm onSubmit={loginMock} />
+      </TestWrapper>
+    );
+
+    await user.type(screen.getByTestId('email-input'), 'test@example.com');
+    await user.type(screen.getByTestId('password-input'), 'password123');
+    await user.click(screen.getByTestId('submit-button'));
+
+    await waitFor(() => {
+      expect(loginMock).toHaveBeenCalledWith({
+        email: 'test@example.com',
+        password: 'password123'
+      });
+    });
+  });
+
+  it('应该在提交时显示加载状态', async () => {
+    const slowLoginMock = vi.fn().mockImplementation(
+      () => new Promise(resolve => setTimeout(() => resolve({ success: true }), 100))
+    );
+
+    render(
+      <TestWrapper>
+        <LoginForm onSubmit={slowLoginMock} />
+      </TestWrapper>
+    );
+
+    await user.type(screen.getByTestId('email-input'), 'test@example.com');
+    await user.type(screen.getByTestId('password-input'), 'password123');
+    await user.click(screen.getByTestId('submit-button'));
+
+    expect(screen.getByTestId('submit-button')).toBeDisabled();
+    expect(screen.getByTestId('submit-button')).toHaveTextContent('Logging in...');
+
+    await waitFor(() => {
+      expect(screen.getByTestId('submit-button')).not.toBeDisabled();
+    });
+  });
+
+  it('应该与React Router集成', () => {
+    render(
+      <TestQueryProvider>
+        <TestRouter initialPath="/login">
+          <LoginPage />
+        </TestRouter>
+      </TestQueryProvider>
+    );
+
+    expect(screen.getByRole('heading', { name: 'Login' })).toBeInTheDocument();
+    expect(screen.getByTestId('login-form')).toBeInTheDocument();
+  });
+
+  it('应该处理表单验证错误', async () => {
+    const errorMock = vi.fn().mockRejectedValue(new Error('Invalid credentials'));
+
+    render(
+      <TestWrapper>
+        <LoginForm onSubmit={errorMock} />
+      </TestWrapper>
+    );
+
+    // 不填写必填字段直接提交
+    await user.click(screen.getByTestId('submit-button'));
+
+    // 检查表单仍然可见(没有因为错误而崩溃)
+    expect(screen.getByTestId('login-form')).toBeInTheDocument();
+  });
+
+  it('应该支持键盘导航和辅助功能', async () => {
+    render(
+      <TestWrapper>
+        <LoginForm onSubmit={loginMock} />
+      </TestWrapper>
+    );
+
+    const emailInput = screen.getByTestId('email-input');
+    const passwordInput = screen.getByTestId('password-input');
+    const submitButton = screen.getByTestId('submit-button');
+
+    // Tab 导航
+    await user.tab();
+    expect(emailInput).toHaveFocus();
+
+    await user.tab();
+    expect(passwordInput).toHaveFocus();
+
+    await user.tab();
+    expect(submitButton).toHaveFocus();
+
+    // Enter 提交
+    await user.type(emailInput, 'test@example.com');
+    await user.type(passwordInput, 'password123');
+    await user.keyboard('{Enter}');
+
+    await waitFor(() => {
+      expect(loginMock).toHaveBeenCalled();
+    });
+  });
+});

+ 91 - 0
src/client/__test_utils__/test-query.tsx

@@ -0,0 +1,91 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactNode } from 'react';
+import { vi } from 'vitest';
+
+/**
+ * 创建测试用的QueryClient(带默认配置)
+ */
+export function createTestQueryClient(options = {}) {
+  return new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+        gcTime: 0,
+        staleTime: 0,
+      },
+      mutations: {
+        retry: false,
+      },
+    },
+    ...options
+  });
+}
+
+/**
+ * QueryProvider包装组件
+ */
+export function TestQueryProvider({
+  children,
+  client
+}: {
+  children: ReactNode;
+  client?: QueryClient
+}) {
+  const queryClient = client || createTestQueryClient();
+
+  return (
+    <QueryClientProvider client={queryClient}>
+      {children}
+    </QueryClientProvider>
+  );
+}
+
+/**
+ * Mock查询Hook
+ */
+export function mockUseQuery(data: any, isLoading = false, error: any = null) {
+  return vi.fn().mockReturnValue({
+    data,
+    isLoading,
+    isError: !!error,
+    error,
+    isSuccess: !isLoading && !error,
+    refetch: vi.fn(),
+  });
+}
+
+/**
+ * Mock变更Hook
+ */
+export function mockUseMutation() {
+  return vi.fn().mockReturnValue({
+    mutate: vi.fn(),
+    mutateAsync: vi.fn().mockResolvedValue({}),
+    isLoading: false,
+    isError: false,
+    error: null,
+    isSuccess: false,
+    reset: vi.fn(),
+  });
+}
+
+/**
+ * 等待查询完成
+ */
+export async function waitForQueryToFinish(delay = 100) {
+  await new Promise(resolve => setTimeout(resolve, delay));
+}
+
+/**
+ * 模拟网络错误
+ */
+export function mockNetworkError() {
+  return new Error('Network error');
+}
+
+/**
+ * 模拟服务器错误
+ */
+export function mockServerError() {
+  return new Error('Server error');
+}

+ 47 - 0
src/client/__test_utils__/test-router.tsx

@@ -0,0 +1,47 @@
+import { ReactNode } from 'react';
+import { MemoryRouter, Routes, Route, useLocation } from 'react-router-dom';
+import { vi } from 'vitest';
+
+/**
+ * 测试路由器的包装组件
+ */
+export function TestRouter({
+  children,
+  initialPath = '/',
+  routes = []
+}: {
+  children: ReactNode;
+  initialPath?: string;
+  routes?: Array<{ path: string; element: ReactNode }>;
+}) {
+  return (
+    <MemoryRouter initialEntries={[initialPath]}>
+      <Routes>
+        {routes.map((route, index) => (
+          <Route key={index} path={route.path} element={route.element} />
+        ))}
+        <Route path="*" element={children} />
+      </Routes>
+    </MemoryRouter>
+  );
+}
+
+/**
+ * 获取当前路由位置的Hook
+ */
+export function useTestLocation() {
+  const location = useLocation();
+  return location;
+}
+
+/**
+ * 创建测试导航函数
+ */
+export function createTestNavigation() {
+  return {
+    navigate: vi.fn(),
+    goBack: vi.fn(),
+    goForward: vi.fn(),
+    replace: vi.fn()
+  };
+}

+ 164 - 0
src/server/__test_utils__/service-mocks.ts

@@ -0,0 +1,164 @@
+import { vi } from 'vitest';
+
+/**
+ * 创建HTTP服务mock
+ */
+export function createHttpServiceMock() {
+  return {
+    get: vi.fn().mockResolvedValue({ data: {}, status: 200 }),
+    post: vi.fn().mockResolvedValue({ data: {}, status: 201 }),
+    put: vi.fn().mockResolvedValue({ data: {}, status: 200 }),
+    delete: vi.fn().mockResolvedValue({ data: {}, status: 204 }),
+    patch: vi.fn().mockResolvedValue({ data: {}, status: 200 }),
+  };
+}
+
+/**
+ * 创建认证服务mock
+ */
+export function createAuthServiceMock() {
+  return {
+    verifyToken: vi.fn().mockResolvedValue({ userId: 1, username: 'testuser' }),
+    generateToken: vi.fn().mockReturnValue('mock-jwt-token'),
+    refreshToken: vi.fn().mockResolvedValue({ accessToken: 'new-token', refreshToken: 'new-refresh-token' }),
+    invalidateToken: vi.fn().mockResolvedValue(undefined),
+  };
+}
+
+/**
+ * 创建邮件服务mock
+ */
+export function createEmailServiceMock() {
+  return {
+    sendWelcomeEmail: vi.fn().mockResolvedValue({ success: true }),
+    sendPasswordResetEmail: vi.fn().mockResolvedValue({ success: true }),
+    sendNotification: vi.fn().mockResolvedValue({ success: true }),
+  };
+}
+
+/**
+ * 创建文件存储服务mock
+ */
+export function createStorageServiceMock() {
+  return {
+    uploadFile: vi.fn().mockResolvedValue({ url: 'https://example.com/file.jpg', key: 'file-key' }),
+    deleteFile: vi.fn().mockResolvedValue({ success: true }),
+    getFileUrl: vi.fn().mockReturnValue('https://example.com/file.jpg'),
+    listFiles: vi.fn().mockResolvedValue([]),
+  };
+}
+
+/**
+ * 创建支付服务mock
+ */
+export function createPaymentServiceMock() {
+  return {
+    createPaymentIntent: vi.fn().mockResolvedValue({ clientSecret: 'pi_mock_secret', id: 'pi_mock_id' }),
+    confirmPayment: vi.fn().mockResolvedValue({ success: true, transactionId: 'txn_mock_id' }),
+    refundPayment: vi.fn().mockResolvedValue({ success: true, refundId: 'ref_mock_id' }),
+  };
+}
+
+/**
+ * 创建短信服务mock
+ */
+export function createSmsServiceMock() {
+  return {
+    sendVerificationCode: vi.fn().mockResolvedValue({ success: true, messageId: 'sms_mock_id' }),
+    sendNotification: vi.fn().mockResolvedValue({ success: true, messageId: 'sms_mock_id' }),
+  };
+}
+
+/**
+ * 创建第三方API服务mock
+ */
+export function createThirdPartyApiMock() {
+  return {
+    call: vi.fn().mockResolvedValue({ success: true, data: {} }),
+    validate: vi.fn().mockResolvedValue({ valid: true, errors: [] }),
+    webhook: vi.fn().mockResolvedValue({ received: true }),
+  };
+}
+
+/**
+ * 模拟网络延迟
+ */
+export function mockNetworkDelay(delayMs: number) {
+  return new Promise(resolve => setTimeout(resolve, delayMs));
+}
+
+/**
+ * 模拟HTTP错误响应
+ */
+export function mockHttpError(status: number, message: string) {
+  return {
+    response: {
+      status,
+      data: { error: message }
+    }
+  };
+}
+
+/**
+ * 模拟超时错误
+ */
+export function mockTimeoutError() {
+  return new Error('Request timeout');
+}
+
+/**
+ * 模拟网络断开错误
+ */
+export function mockNetworkError() {
+  return new Error('Network error');
+}
+
+/**
+ * 服务mock工具类
+ */
+export class ServiceMocks {
+  static http = createHttpServiceMock();
+  static auth = createAuthServiceMock();
+  static email = createEmailServiceMock();
+  static storage = createStorageServiceMock();
+  static payment = createPaymentServiceMock();
+  static sms = createSmsServiceMock();
+  static thirdParty = createThirdPartyApiMock();
+
+  /**
+   * 重置所有mock
+   */
+  static resetAll() {
+    Object.values(this).forEach(service => {
+      if (service && typeof service === 'object') {
+        Object.values(service).forEach(mock => {
+          if (mock && typeof mock === 'function' && 'mockClear' in mock) {
+            mock.mockClear();
+          }
+        });
+      }
+    });
+  }
+
+  /**
+   * 设置所有mock为成功状态
+   */
+  static setupForSuccess() {
+    this.resetAll();
+    // 所有mock默认已经是成功状态
+  }
+
+  /**
+   * 设置所有mock为失败状态
+   */
+  static setupForFailure() {
+    this.resetAll();
+    Object.values(this.http).forEach(mock => mock.mockRejectedValue(mockNetworkError()));
+    Object.values(this.auth).forEach(mock => mock.mockRejectedValue(new Error('Auth failed')));
+    Object.values(this.email).forEach(mock => mock.mockRejectedValue(new Error('Email failed')));
+    Object.values(this.storage).forEach(mock => mock.mockRejectedValue(new Error('Storage failed')));
+    Object.values(this.payment).forEach(mock => mock.mockRejectedValue(new Error('Payment failed')));
+    Object.values(this.sms).forEach(mock => mock.mockRejectedValue(new Error('SMS failed')));
+    Object.values(this.thirdParty).forEach(mock => mock.mockRejectedValue(new Error('Third party failed')));
+  }
+}

+ 5 - 1
src/server/__test_utils__/test-db.ts

@@ -113,7 +113,11 @@ export class TestDatabase {
       database: ':memory:',
       synchronize: true,
       logging: false,
-      entities: [] // 需要根据实际项目配置实体
+      entities: [
+        // 导入实际实体
+        (await import('../modules/users/user.entity')).UserEntity,
+        (await import('../modules/users/role.entity')).Role
+      ]
     });
 
     await this.dataSource.initialize();

+ 5 - 1
src/test/setup.ts

@@ -1,8 +1,12 @@
 // 测试环境全局设置
-import { beforeAll, afterAll, afterEach, vi } from 'vitest';
+import { beforeAll, afterAll, afterEach, vi, expect } from 'vitest';
+import * as matchers from '@testing-library/jest-dom/matchers';
 
 // 全局测试超时设置已在 vitest.config.ts 中配置
 
+// 扩展expect匹配器
+expect.extend(matchers);
+
 // 全局测试前置处理
 beforeAll(() => {
   // 设置测试环境变量