2
0
Эх сурвалжийг харах

📝 docs(testing): 添加集成测试最佳实践文档和测试工具

- 创建集成测试最佳实践文档,包含测试类型区分、API测试模式、React组件测试模式、Mock策略等
- 添加测试工具类:API客户端、服务stub、认证工具、数据库工具、测试服务器
- 配置组件测试专用的Vitest配置文件
- 添加用户API和基础API的集成测试示例
- 更新package.json添加组件和集成测试脚本
- 更新Claude设置允许测试相关命令

✨ feat(testing): 添加React组件集成测试支持

- 添加@testing-library/react和@testing-library/user-event依赖
- 创建Button组件集成测试示例
- 添加测试渲染工具类TestWrapper和createTestQueryClient

🔧 chore(deps): 更新测试相关依赖

- 添加测试库相关依赖到pnpm-lock.yaml
- 包括@testing-library/react、@testing-library/user-event及相关工具
yourname 2 сар өмнө
parent
commit
030e91590e

+ 3 - 1
.claude/settings.local.json

@@ -6,7 +6,9 @@
       "Bash(pnpm test:*)",
       "Bash(sed:*)",
       "Bash(pnpm run lint)",
-      "Bash(find:*)"
+      "Bash(find:*)",
+      "Bash(npm run test:*)",
+      "Bash(npx vitest:*)"
     ],
     "deny": [],
     "ask": []

+ 364 - 0
docs/integration-testing-best-practices.md

@@ -0,0 +1,364 @@
+# 集成测试最佳实践
+
+## 概述
+
+本文档提供了在项目中编写和维护集成测试的最佳实践指南。集成测试用于验证多个组件或服务之间的协作是否正确。
+
+## 测试类型区分
+
+### 单元测试 vs 集成测试
+
+| 测试类型 | 测试范围 | 使用场景 |
+|---------|---------|---------|
+| **单元测试** | 单个函数、方法、类 | 验证独立逻辑的正确性 |
+| **集成测试** | 多个组件、服务协作 | 验证系统各部分集成是否正确 |
+| **端到端测试** | 完整用户流程 | 验证从用户界面到后端的完整流程 |
+
+## API集成测试模式
+
+### 基本测试结构
+
+```typescript
+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();
+  });
+});
+```
+
+### 认证和授权测试
+
+```typescript
+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);
+  });
+});
+```
+
+### 错误处理测试
+
+```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');
+  });
+});
+```
+
+## 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('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);
+  });
+});
+```
+
+### 路由和导航测试
+
+```typescript
+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();
+  });
+});
+```
+
+### 状态管理测试
+
+```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();
+    });
+  });
+});
+```
+
+## Mock策略
+
+### 数据库Mock
+
+```typescript
+// 使用内存数据库
+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
+
+```typescript
+// 使用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
+
+```typescript
+// Mock JWT验证
+vi.mock('../auth/middleware', () => ({
+  authMiddleware: vi.fn().mockImplementation((c, next) => {
+    c.set('user', { id: 1, username: 'testuser' });
+    return next();
+  })
+}));
+```
+
+## 测试数据管理
+
+### 测试数据工厂
+
+```typescript
+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' });
+```
+
+### 测试数据清理
+
+```typescript
+afterEach(async () => {
+  // 清理测试数据
+  await testDataSource.getRepository(User).clear();
+  await testDataSource.getRepository(Role).clear();
+});
+
+afterAll(async () => {
+  // 关闭数据库连接
+  await testDataSource.destroy();
+});
+```
+
+## 性能优化
+
+### 测试并行化
+
+```typescript
+// 在vitest.config.ts中配置
+export default defineConfig({
+  test: {
+    maxThreads: 4,
+    minThreads: 2,
+    fileParallelism: true
+  }
+});
+```
+
+### 测试数据隔离
+
+```typescript
+// 使用事务确保测试隔离
+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. 测试相互干扰
+**问题**: 一个测试修改了全局状态,影响其他测试
+**解决方案**: 使用`beforeEach`和`afterEach`清理状态
+
+### 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内存数据库
+
+### 配置文件示例
+
+```typescript
+// vitest.config.ts
+export default defineConfig({
+  test: {
+    environment: 'node',
+    include: ['**/__integration_tests__/**/*.test.ts'],
+    setupFiles: ['./src/test/setup.ts'],
+    globals: true
+  }
+});
+```
+
+## 总结
+
+集成测试是确保系统各部分正确协作的关键。遵循这些最佳实践可以编写出可靠、可维护的集成测试。记住测试的目标是增加信心,而不是追求100%的覆盖率。专注于测试关键业务逻辑和集成点。

+ 4 - 0
package.json

@@ -14,6 +14,8 @@
     "test:watch": "vitest",
     "test:ui": "vitest src/client/__tests__",
     "test:api": "vitest src/server/__tests__",
+    "test:components": "vitest --config=vitest.config.components.ts",
+    "test:integration": "vitest src/server/__integration_tests__ src/client/__integration_tests__",
     "test:e2e": "playwright test --config=tests/e2e/playwright.config.ts",
     "test:e2e:ui": "playwright test --config=tests/e2e/playwright.config.ts --ui",
     "test:e2e:debug": "playwright test --config=tests/e2e/playwright.config.ts --debug",
@@ -99,6 +101,8 @@
   "devDependencies": {
     "@playwright/test": "^1.55.0",
     "@tailwindcss/vite": "^4.1.11",
+    "@testing-library/react": "^16.3.0",
+    "@testing-library/user-event": "^14.6.1",
     "@types/bcrypt": "^6.0.0",
     "@types/debug": "^4.1.12",
     "@types/jsonwebtoken": "^9.0.10",

+ 116 - 0
pnpm-lock.yaml

@@ -216,6 +216,12 @@ importers:
       '@tailwindcss/vite':
         specifier: ^4.1.11
         version: 4.1.11(vite@7.0.6(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.8.0))
+      '@testing-library/react':
+        specifier: ^16.3.0
+        version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+      '@testing-library/user-event':
+        specifier: ^14.6.1
+        version: 14.6.1(@testing-library/dom@10.4.1)
       '@types/bcrypt':
         specifier: ^6.0.0
         version: 6.0.0
@@ -296,6 +302,10 @@ packages:
     peerDependencies:
       zod: ^4.0.0
 
+  '@babel/code-frame@7.27.1':
+    resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/helper-string-parser@7.27.1':
     resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
     engines: {node: '>=6.9.0'}
@@ -1496,10 +1506,38 @@ packages:
     peerDependencies:
       react: ^18 || ^19
 
+  '@testing-library/dom@10.4.1':
+    resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
+    engines: {node: '>=18'}
+
+  '@testing-library/react@16.3.0':
+    resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@testing-library/dom': ^10.0.0
+      '@types/react': ^18.0.0 || ^19.0.0
+      '@types/react-dom': ^18.0.0 || ^19.0.0
+      react: ^18.0.0 || ^19.0.0
+      react-dom: ^18.0.0 || ^19.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@testing-library/user-event@14.6.1':
+    resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==}
+    engines: {node: '>=12', npm: '>=6'}
+    peerDependencies:
+      '@testing-library/dom': '>=7.21.4'
+
   '@tootallnate/once@2.0.0':
     resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
     engines: {node: '>= 10'}
 
+  '@types/aria-query@5.0.4':
+    resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
   '@types/bcrypt@6.0.0':
     resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
 
@@ -1640,6 +1678,10 @@ packages:
     resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
     engines: {node: '>=8'}
 
+  ansi-styles@5.2.0:
+    resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
+    engines: {node: '>=10'}
+
   ansi-styles@6.2.1:
     resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
     engines: {node: '>=12'}
@@ -1656,6 +1698,9 @@ packages:
     resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
     engines: {node: '>=10'}
 
+  aria-query@5.3.0:
+    resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
   assertion-error@2.0.1:
     resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
     engines: {node: '>=12'}
@@ -1900,6 +1945,10 @@ packages:
     resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
     engines: {node: '>=0.10'}
 
+  dequal@2.0.3:
+    resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+    engines: {node: '>=6'}
+
   detect-libc@2.0.4:
     resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
     engines: {node: '>=8'}
@@ -1907,6 +1956,9 @@ packages:
   detect-node-es@1.1.0:
     resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
 
+  dom-accessibility-api@0.5.16:
+    resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
   dom-helpers@5.2.1:
     resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
 
@@ -2350,6 +2402,10 @@ packages:
     peerDependencies:
       react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
 
+  lz-string@1.5.0:
+    resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+    hasBin: true
+
   magic-string@0.30.17:
     resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
 
@@ -2494,6 +2550,10 @@ packages:
     resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
     engines: {node: ^10 || ^12 || >=14}
 
+  pretty-format@27.5.1:
+    resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+    engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+
   prop-types@15.8.1:
     resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
 
@@ -2530,6 +2590,9 @@ packages:
   react-is@16.13.1:
     resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
 
+  react-is@17.0.2:
+    resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+
   react-is@18.3.1:
     resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
 
@@ -3126,6 +3189,12 @@ snapshots:
       openapi3-ts: 4.5.0
       zod: 4.0.15
 
+  '@babel/code-frame@7.27.1':
+    dependencies:
+      '@babel/helper-validator-identifier': 7.27.1
+      js-tokens: 4.0.0
+      picocolors: 1.1.1
+
   '@babel/helper-string-parser@7.27.1': {}
 
   '@babel/helper-validator-identifier@7.27.1': {}
@@ -4175,9 +4244,36 @@ snapshots:
       '@tanstack/query-core': 5.83.0
       react: 19.1.0
 
+  '@testing-library/dom@10.4.1':
+    dependencies:
+      '@babel/code-frame': 7.27.1
+      '@babel/runtime': 7.28.2
+      '@types/aria-query': 5.0.4
+      aria-query: 5.3.0
+      dom-accessibility-api: 0.5.16
+      lz-string: 1.5.0
+      picocolors: 1.1.1
+      pretty-format: 27.5.1
+
+  '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+    dependencies:
+      '@babel/runtime': 7.28.2
+      '@testing-library/dom': 10.4.1
+      react: 19.1.0
+      react-dom: 19.1.0(react@19.1.0)
+    optionalDependencies:
+      '@types/react': 19.1.8
+      '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+  '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
+    dependencies:
+      '@testing-library/dom': 10.4.1
+
   '@tootallnate/once@2.0.0':
     optional: true
 
+  '@types/aria-query@5.0.4': {}
+
   '@types/bcrypt@6.0.0':
     dependencies:
       '@types/node': 24.1.0
@@ -4344,6 +4440,8 @@ snapshots:
     dependencies:
       color-convert: 2.0.1
 
+  ansi-styles@5.2.0: {}
+
   ansi-styles@6.2.1: {}
 
   ansis@3.17.0: {}
@@ -4354,6 +4452,10 @@ snapshots:
     dependencies:
       tslib: 2.8.1
 
+  aria-query@5.3.0:
+    dependencies:
+      dequal: 2.0.3
+
   assertion-error@2.0.1: {}
 
   ast-v8-to-istanbul@0.3.5:
@@ -4588,10 +4690,14 @@ snapshots:
 
   denque@2.1.0: {}
 
+  dequal@2.0.3: {}
+
   detect-libc@2.0.4: {}
 
   detect-node-es@1.1.0: {}
 
+  dom-accessibility-api@0.5.16: {}
+
   dom-helpers@5.2.1:
     dependencies:
       '@babel/runtime': 7.28.2
@@ -5046,6 +5152,8 @@ snapshots:
     dependencies:
       react: 19.1.0
 
+  lz-string@1.5.0: {}
+
   magic-string@0.30.17:
     dependencies:
       '@jridgewell/sourcemap-codec': 1.5.4
@@ -5166,6 +5274,12 @@ snapshots:
       picocolors: 1.1.1
       source-map-js: 1.2.1
 
+  pretty-format@27.5.1:
+    dependencies:
+      ansi-regex: 5.0.1
+      ansi-styles: 5.2.0
+      react-is: 17.0.2
+
   prop-types@15.8.1:
     dependencies:
       loose-envify: 1.4.0
@@ -5203,6 +5317,8 @@ snapshots:
 
   react-is@16.13.1: {}
 
+  react-is@17.0.2: {}
+
   react-is@18.3.1: {}
 
   react-remove-scroll-bar@2.3.8(@types/react@19.1.8)(react@19.1.0):

+ 62 - 0
src/client/__integration_tests__/components/Button.integration.test.tsx

@@ -0,0 +1,62 @@
+import { describe, it, expect } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { TestWrapper } from '../../__test_utils__/test-render';
+
+// 假设有一个简单的Button组件
+function Button({ onClick, children }: { onClick: () => void; children: React.ReactNode }) {
+  return (
+    <button
+      onClick={onClick}
+      className="bg-blue-500 text-white px-4 py-2 rounded"
+      data-testid="test-button"
+    >
+      {children}
+    </button>
+  );
+}
+
+describe('Button Component Integration Tests', () => {
+  it('应该渲染按钮并显示正确的文本', () => {
+    render(
+      <TestWrapper>
+        <Button onClick={() => {}}>Click Me</Button>
+      </TestWrapper>
+    );
+
+    const button = screen.getByTestId('test-button');
+    expect(button).toBeInTheDocument();
+    expect(button).toHaveTextContent('Click Me');
+  });
+
+  it('应该在点击时调用onClick处理函数', async () => {
+    const user = userEvent.setup();
+    const handleClick = vi.fn();
+
+    render(
+      <TestWrapper>
+        <Button onClick={handleClick}>Click Me</Button>
+      </TestWrapper>
+    );
+
+    const button = screen.getByTestId('test-button');
+    await user.click(button);
+
+    expect(handleClick).toHaveBeenCalledTimes(1);
+  });
+
+  it('应该具有正确的样式类', () => {
+    render(
+      <TestWrapper>
+        <Button onClick={() => {}}>Click Me</Button>
+      </TestWrapper>
+    );
+
+    const button = screen.getByTestId('test-button');
+    expect(button).toHaveClass('bg-blue-500');
+    expect(button).toHaveClass('text-white');
+    expect(button).toHaveClass('px-4');
+    expect(button).toHaveClass('py-2');
+    expect(button).toHaveClass('rounded');
+  });
+});

+ 45 - 0
src/client/__test_utils__/test-render.tsx

@@ -0,0 +1,45 @@
+import { ReactNode } from 'react';
+import { BrowserRouter } from 'react-router-dom';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ThemeProvider } from 'next-themes';
+
+/**
+ * 创建测试用的QueryClient
+ */
+export function createTestQueryClient() {
+  return new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+        gcTime: 0,
+      },
+      mutations: {
+        retry: false,
+      },
+    }
+  });
+}
+
+/**
+ * 测试渲染器的包装组件
+ */
+export function TestWrapper({ children }: { children: ReactNode }) {
+  const queryClient = createTestQueryClient();
+
+  return (
+    <QueryClientProvider client={queryClient}>
+      <ThemeProvider attribute="class" defaultTheme="light">
+        <BrowserRouter>
+          {children}
+        </BrowserRouter>
+      </ThemeProvider>
+    </QueryClientProvider>
+  );
+}
+
+/**
+ * 等待组件更新完成
+ */
+export async function waitForUpdate(delay = 0) {
+  await new Promise(resolve => setTimeout(resolve, delay));
+}

+ 192 - 0
src/server/__test_utils__/api-client.ts

@@ -0,0 +1,192 @@
+import { createTestServer } from './test-server';
+
+export interface ApiClientOptions {
+  baseURL?: string;
+  headers?: Record<string, string>;
+  authToken?: string;
+}
+
+export class ApiClient {
+  private app: any;
+  private options: ApiClientOptions;
+
+  constructor(app: any, options: ApiClientOptions = {}) {
+    this.app = app;
+    this.options = {
+      baseURL: 'http://localhost:3000',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      ...options
+    };
+
+    if (this.options.authToken) {
+      this.options.headers = {
+        ...this.options.headers,
+        'Authorization': `Bearer ${this.options.authToken}`
+      };
+    }
+  }
+
+  /**
+   * 发送GET请求
+   */
+  async get<T = any>(path: string, headers?: Record<string, string>): Promise<ApiResponse<T>> {
+    const server = createTestServer(this.app);
+    const response = await server.get(path, {
+      ...this.options.headers,
+      ...headers
+    });
+
+    return this.createResponse(response);
+  }
+
+  /**
+   * 发送POST请求
+   */
+  async post<T = any>(path: string, data?: any, headers?: Record<string, string>): Promise<ApiResponse<T>> {
+    const server = createTestServer(this.app);
+    const response = await server.post(path, data, {
+      ...this.options.headers,
+      ...headers
+    });
+
+    return this.createResponse(response);
+  }
+
+  /**
+   * 发送PUT请求
+   */
+  async put<T = any>(path: string, data?: any, headers?: Record<string, string>): Promise<ApiResponse<T>> {
+    const server = createTestServer(this.app);
+    const response = await server.put(path, data, {
+      ...this.options.headers,
+      ...headers
+    });
+
+    return this.createResponse(response);
+  }
+
+  /**
+   * 发送DELETE请求
+   */
+  async delete<T = any>(path: string, headers?: Record<string, string>): Promise<ApiResponse<T>> {
+    const server = createTestServer(this.app);
+    const response = await server.delete(path, {
+      ...this.options.headers,
+      ...headers
+    });
+
+    return this.createResponse(response);
+  }
+
+  /**
+   * 发送PATCH请求
+   */
+  async patch<T = any>(path: string, data?: any, headers?: Record<string, string>): Promise<ApiResponse<T>> {
+    const server = createTestServer(this.app);
+    const response = await server.patch(path, data, {
+      ...this.options.headers,
+      ...headers
+    });
+
+    return this.createResponse(response);
+  }
+
+  /**
+   * 设置认证令牌
+   */
+  setAuthToken(token: string): void {
+    this.options.authToken = token;
+    if (this.options.headers) {
+      this.options.headers.Authorization = `Bearer ${token}`;
+    }
+  }
+
+  /**
+   * 清除认证令牌
+   */
+  clearAuthToken(): void {
+    this.options.authToken = undefined;
+    if (this.options.headers) {
+      delete this.options.headers.Authorization;
+    }
+  }
+
+  /**
+   * 添加请求头
+   */
+  setHeader(name: string, value: string): void {
+    if (!this.options.headers) {
+      this.options.headers = {};
+    }
+    this.options.headers[name] = value;
+  }
+
+  /**
+   * 移除请求头
+   */
+  removeHeader(name: string): void {
+    if (this.options.headers) {
+      delete this.options.headers[name];
+    }
+  }
+
+  private createResponse<T>(response: any): ApiResponse<T> {
+    return {
+      status: response.status,
+      headers: response.headers,
+      data: response.data,
+      ok: response.status >= 200 && response.status < 300,
+      json: async () => response.data,
+      text: async () => typeof response.data === 'string' ? response.data : JSON.stringify(response.data)
+    };
+  }
+}
+
+export interface ApiResponse<T = any> {
+  status: number;
+  headers: Record<string, string>;
+  data: T;
+  ok: boolean;
+  json: () => Promise<T>;
+  text: () => Promise<string>;
+}
+
+/**
+ * 创建API客户端工厂函数
+ */
+export function createApiClient(app: any, options?: ApiClientOptions): ApiClient {
+  return new ApiClient(app, options);
+}
+
+/**
+ * 断言响应状态码
+ */
+export function expectStatus(response: ApiResponse, expectedStatus: number): void {
+  if (response.status !== expectedStatus) {
+    throw new Error(`Expected status ${expectedStatus}, but got ${response.status}. Response: ${JSON.stringify(response.data)}`);
+  }
+}
+
+/**
+ * 断言响应包含特定字段
+ */
+export function expectResponseToHave<T>(response: ApiResponse<T>, expectedFields: Partial<T>): void {
+  for (const [key, value] of Object.entries(expectedFields)) {
+    if ((response.data as any)[key] !== value) {
+      throw new Error(`Expected field ${key} to be ${value}, but got ${(response.data as any)[key]}`);
+    }
+  }
+}
+
+/**
+ * 断言响应包含特定结构
+ */
+export function expectResponseStructure(response: ApiResponse, structure: Record<string, any>): void {
+  for (const key of Object.keys(structure)) {
+    if (!(key in response.data)) {
+      throw new Error(`Expected response to have key: ${key}`);
+    }
+  }
+}

+ 159 - 0
src/server/__test_utils__/service-stubs.ts

@@ -0,0 +1,159 @@
+import { vi } from 'vitest';
+
+/**
+ * 创建模拟的用户服务
+ */
+export function createMockUserService() {
+  return {
+    getUsersWithPagination: vi.fn().mockResolvedValue([[], 0]),
+    getUserById: vi.fn().mockResolvedValue(null),
+    getUserByUsername: vi.fn().mockResolvedValue(null),
+    getUserByEmail: vi.fn().mockResolvedValue(null),
+    createUser: vi.fn().mockResolvedValue({ id: 1, username: 'testuser' }),
+    updateUser: vi.fn().mockResolvedValue({ affected: 1 }),
+    deleteUser: vi.fn().mockResolvedValue({ affected: 1 }),
+    changePassword: vi.fn().mockResolvedValue({ affected: 1 }),
+    verifyPassword: vi.fn().mockResolvedValue(true),
+    assignRole: vi.fn().mockResolvedValue({ affected: 1 }),
+    removeRole: vi.fn().mockResolvedValue({ affected: 1 })
+  };
+}
+
+/**
+ * 创建模拟的认证服务
+ */
+export function createMockAuthService() {
+  return {
+    login: vi.fn().mockResolvedValue({
+      token: 'mock-jwt-token',
+      user: { id: 1, username: 'testuser' }
+    }),
+    logout: vi.fn().mockResolvedValue(undefined),
+    register: vi.fn().mockResolvedValue({ id: 1, username: 'newuser' }),
+    verifyToken: vi.fn().mockResolvedValue({ id: 1, username: 'testuser' }),
+    refreshToken: vi.fn().mockResolvedValue({ token: 'new-mock-jwt-token' }),
+    forgotPassword: vi.fn().mockResolvedValue({ success: true }),
+    resetPassword: vi.fn().mockResolvedValue({ success: true })
+  };
+}
+
+/**
+ * 创建模拟的角色服务
+ */
+export function createMockRoleService() {
+  return {
+    getRoles: vi.fn().mockResolvedValue([]),
+    getRoleById: vi.fn().mockResolvedValue(null),
+    createRole: vi.fn().mockResolvedValue({ id: 1, name: 'admin' }),
+    updateRole: vi.fn().mockResolvedValue({ affected: 1 }),
+    deleteRole: vi.fn().mockResolvedValue({ affected: 1 }),
+    assignPermission: vi.fn().mockResolvedValue({ affected: 1 }),
+    removePermission: vi.fn().mockResolvedValue({ affected: 1 })
+  };
+}
+
+/**
+ * 创建模拟的通用CRUD服务
+ */
+export function createMockCrudService() {
+  return {
+    findAll: vi.fn().mockResolvedValue([]),
+    findOne: vi.fn().mockResolvedValue(null),
+    create: vi.fn().mockResolvedValue({ id: 1 }),
+    update: vi.fn().mockResolvedValue({ affected: 1 }),
+    delete: vi.fn().mockResolvedValue({ affected: 1 }),
+    count: vi.fn().mockResolvedValue(0),
+    exists: vi.fn().mockResolvedValue(false)
+  };
+}
+
+/**
+ * 创建模拟的邮件服务
+ */
+export function createMockEmailService() {
+  return {
+    sendWelcomeEmail: vi.fn().mockResolvedValue({ success: true }),
+    sendPasswordResetEmail: vi.fn().mockResolvedValue({ success: true }),
+    sendNotification: vi.fn().mockResolvedValue({ success: true }),
+    verifyEmail: vi.fn().mockResolvedValue({ success: true })
+  };
+}
+
+/**
+ * 创建模拟的文件服务
+ */
+export function createMockFileService() {
+  return {
+    upload: vi.fn().mockResolvedValue({ url: 'https://example.com/file.jpg' }),
+    download: vi.fn().mockResolvedValue(Buffer.from('test content')),
+    delete: vi.fn().mockResolvedValue({ success: true }),
+    getSignedUrl: vi.fn().mockResolvedValue('https://example.com/signed-url')
+  };
+}
+
+/**
+ * 服务stub工具类
+ */
+export class ServiceStubManager {
+  private stubs = new Map<string, any>();
+
+  /**
+   * 创建服务stub
+   */
+  createStub<T>(serviceName: string, stubImplementation: Partial<T>): T {
+    const stub = { ...stubImplementation } as T;
+    this.stubs.set(serviceName, stub);
+    return stub;
+  }
+
+  /**
+   * 获取服务stub
+   */
+  getStub<T>(serviceName: string): T | undefined {
+    return this.stubs.get(serviceName);
+  }
+
+  /**
+   * 重置所有stub
+   */
+  resetAll(): void {
+    this.stubs.clear();
+  }
+
+  /**
+   * 重置特定stub
+   */
+  resetStub(serviceName: string): void {
+    this.stubs.delete(serviceName);
+  }
+}
+
+/**
+ * 全局服务stub管理器
+ */
+export const serviceStubs = new ServiceStubManager();
+
+/**
+ * 设置服务mock
+ */
+export function setupServiceMocks() {
+  // 用户服务mock
+  vi.mock('../modules/users/user.service', () => ({
+    UserService: vi.fn().mockImplementation(() => serviceStubs.createStub('UserService', createMockUserService()))
+  }));
+
+  // 认证服务mock
+  vi.mock('../modules/auth/auth.service', () => ({
+    AuthService: vi.fn().mockImplementation(() => serviceStubs.createStub('AuthService', createMockAuthService()))
+  }));
+
+  // 角色服务mock
+  vi.mock('../modules/roles/role.service', () => ({
+    RoleService: vi.fn().mockImplementation(() => serviceStubs.createStub('RoleService', createMockRoleService()))
+  }));
+
+  // 通用CRUD服务mock
+  vi.mock('../../utils/generic-crud.service', () => ({
+    GenericCRUDService: vi.fn().mockImplementation(() => serviceStubs.createStub('GenericCRUDService', createMockCrudService()))
+  }));
+}

+ 88 - 0
src/server/__test_utils__/test-auth.ts

@@ -0,0 +1,88 @@
+import { vi } from 'vitest';
+
+/**
+ * 创建模拟的认证上下文
+ */
+export function createMockAuthContext(overrides: Partial<any> = {}) {
+  const baseContext = {
+    req: {
+      header: (name: string) => {
+        const headers: Record<string, string> = {
+          'authorization': 'Bearer test-token-123',
+          'content-type': 'application/json',
+          'user-agent': 'vitest/integration-test',
+          'x-request-id': `test_${Math.random().toString(36).substr(2, 9)}`
+        };
+        return headers[name.toLowerCase()] || null;
+      }
+    },
+    set: vi.fn(),
+    json: vi.fn().mockImplementation((data, status = 200) => ({
+      status,
+      body: data
+    })),
+    status: vi.fn().mockReturnThis(),
+    body: vi.fn().mockReturnThis(),
+    env: {
+      NODE_ENV: 'test',
+      DATABASE_URL: process.env.TEST_DATABASE_URL || 'mysql://root:test@localhost:3306/test_d8dai'
+    },
+    var: {},
+    get: vi.fn()
+  };
+
+  return { ...baseContext, ...overrides };
+}
+
+/**
+ * 创建模拟的JWT用户信息
+ */
+export function createMockJwtPayload(overrides: Partial<any> = {}) {
+  return {
+    sub: '1',
+    username: 'testuser',
+    email: 'test@example.com',
+    roles: ['user'],
+    iat: Math.floor(Date.now() / 1000),
+    exp: Math.floor(Date.now() / 1000) + 3600, // 1小时后过期
+    ...overrides
+  };
+}
+
+/**
+ * 创建模拟的认证中间件
+ */
+export function createMockAuthMiddleware() {
+  return vi.fn().mockImplementation(async (c: any, next: () => Promise<void>) => {
+    // 模拟认证用户信息
+    c.set('user', {
+      id: 1,
+      username: 'testuser',
+      email: 'test@example.com',
+      roles: ['user']
+    });
+    await next();
+  });
+}
+
+/**
+ * 创建模拟的权限中间件
+ */
+export function createMockPermissionMiddleware(requiredRoles: string[] = []) {
+  return vi.fn().mockImplementation(async (c: any, next: () => Promise<void>) => {
+    const user = c.get('user');
+
+    if (!user) {
+      return c.json({ error: 'Unauthorized' }, 401);
+    }
+
+    if (requiredRoles.length > 0) {
+      const hasRole = requiredRoles.some(role => user.roles?.includes(role));
+      if (!hasRole) {
+        return c.json({ error: 'Forbidden' }, 403);
+      }
+    }
+
+    await next();
+  });
+}

+ 152 - 0
src/server/__test_utils__/test-db.ts

@@ -0,0 +1,152 @@
+import { DataSource, EntityManager, Repository } from 'typeorm';
+import { vi, beforeEach, afterEach } from 'vitest';
+
+/**
+ * 创建模拟的数据源
+ */
+export function createMockDataSource() {
+  const mockDataSource = {
+    initialize: vi.fn().mockResolvedValue(undefined),
+    destroy: vi.fn().mockResolvedValue(undefined),
+    isInitialized: true,
+    manager: createMockEntityManager(),
+    getRepository: vi.fn().mockImplementation(() => createMockRepository()),
+    createQueryBuilder: vi.fn().mockReturnValue(createMockQueryBuilder()),
+    transaction: vi.fn().mockImplementation(async (callback) => {
+      return callback(mockDataSource.manager);
+    }),
+    synchronize: vi.fn().mockResolvedValue(undefined),
+    dropDatabase: vi.fn().mockResolvedValue(undefined)
+  };
+
+  return mockDataSource;
+}
+
+/**
+ * 创建模拟的实体管理器
+ */
+export function createMockEntityManager(): EntityManager {
+  return {
+    find: vi.fn().mockResolvedValue([]),
+    findOne: vi.fn().mockResolvedValue(null),
+    save: vi.fn().mockImplementation((entity) => Promise.resolve(entity)),
+    update: vi.fn().mockResolvedValue({ affected: 1 }),
+    delete: vi.fn().mockResolvedValue({ affected: 1 }),
+    createQueryBuilder: vi.fn().mockReturnValue(createMockQueryBuilder()),
+    transaction: vi.fn().mockImplementation(async (callback) => {
+      return callback(mockDataSource.manager);
+    }),
+    getRepository: vi.fn().mockImplementation(() => createMockRepository())
+  } as any;
+}
+
+/**
+ * 创建模拟的Repository
+ */
+export function createMockRepository<T extends object = any>(): Repository<T> {
+  return {
+    find: vi.fn().mockResolvedValue([]),
+    findOne: vi.fn().mockResolvedValue(null),
+    findOneBy: vi.fn().mockResolvedValue(null),
+    findOneByOrFail: vi.fn().mockResolvedValue(null),
+    findBy: vi.fn().mockResolvedValue([]),
+    findAndCount: vi.fn().mockResolvedValue([[], 0]),
+    findAndCountBy: vi.fn().mockResolvedValue([[], 0]),
+    save: vi.fn().mockImplementation((entity) => Promise.resolve(entity)),
+    update: vi.fn().mockResolvedValue({ affected: 1 }),
+    delete: vi.fn().mockResolvedValue({ affected: 1 }),
+    create: vi.fn().mockImplementation((entity) => ({ ...entity, id: Date.now() })),
+    createQueryBuilder: vi.fn().mockReturnValue(createMockQueryBuilder()),
+    count: vi.fn().mockResolvedValue(0),
+    countBy: vi.fn().mockResolvedValue(0),
+    exist: vi.fn().mockResolvedValue(false)
+  } as any;
+}
+
+/**
+ * 创建模拟的QueryBuilder
+ */
+export function createMockQueryBuilder() {
+  const mockQueryBuilder = {
+    select: vi.fn().mockReturnThis(),
+    from: vi.fn().mockReturnThis(),
+    where: vi.fn().mockReturnThis(),
+    andWhere: vi.fn().mockReturnThis(),
+    orWhere: vi.fn().mockReturnThis(),
+    leftJoin: vi.fn().mockReturnThis(),
+    innerJoin: vi.fn().mockReturnThis(),
+    orderBy: vi.fn().mockReturnThis(),
+    groupBy: vi.fn().mockReturnThis(),
+    having: vi.fn().mockReturnThis(),
+    skip: vi.fn().mockReturnThis(),
+    take: vi.fn().mockReturnThis(),
+    getMany: vi.fn().mockResolvedValue([]),
+    getOne: vi.fn().mockResolvedValue(null),
+    getCount: vi.fn().mockResolvedValue(0),
+    getRawMany: vi.fn().mockResolvedValue([]),
+    getRawOne: vi.fn().mockResolvedValue(null),
+    execute: vi.fn().mockResolvedValue(undefined),
+    setParameter: vi.fn().mockReturnThis(),
+    setParameters: vi.fn().mockReturnThis()
+  };
+
+  return mockQueryBuilder;
+}
+
+/**
+ * 数据库测试工具类
+ */
+export class TestDatabase {
+  private static dataSource: DataSource | null = null;
+
+  /**
+   * 初始化测试数据库
+   */
+  static async initialize(): Promise<DataSource> {
+    if (this.dataSource?.isInitialized) {
+      return this.dataSource;
+    }
+
+    // 使用SQLite内存数据库进行测试
+    this.dataSource = new DataSource({
+      type: 'better-sqlite3',
+      database: ':memory:',
+      synchronize: true,
+      logging: false,
+      entities: [] // 需要根据实际项目配置实体
+    });
+
+    await this.dataSource.initialize();
+    return this.dataSource;
+  }
+
+  /**
+   * 清理测试数据库
+   */
+  static async cleanup(): Promise<void> {
+    if (this.dataSource?.isInitialized) {
+      await this.dataSource.destroy();
+      this.dataSource = null;
+    }
+  }
+
+  /**
+   * 获取当前数据源
+   */
+  static getDataSource(): DataSource | null {
+    return this.dataSource;
+  }
+}
+
+/**
+ * 测试数据库生命周期钩子
+ */
+export function setupDatabaseHooks() {
+  beforeEach(async () => {
+    await TestDatabase.initialize();
+  });
+
+  afterEach(async () => {
+    await TestDatabase.cleanup();
+  });
+}

+ 119 - 0
src/server/__test_utils__/test-server.ts

@@ -0,0 +1,119 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import { Hono } from 'hono';
+import { DataSource } from 'typeorm';
+
+export interface TestServerOptions {
+  setupAuth?: boolean;
+  setupDatabase?: boolean;
+  setupMiddlewares?: boolean;
+}
+
+/**
+ * 创建测试服务器实例
+ */
+export function createTestServer(
+  app: OpenAPIHono | Hono,
+  options: TestServerOptions = {}
+) {
+  const server = app as any;
+
+  // 设置默认选项
+  const {
+    setupAuth = true
+  } = options;
+
+  return {
+    get: (path: string, headers?: Record<string, string>) =>
+      makeRequest('GET', path, undefined, headers),
+    post: (path: string, body?: any, headers?: Record<string, string>) =>
+      makeRequest('POST', path, body, headers),
+    put: (path: string, body?: any, headers?: Record<string, string>) =>
+      makeRequest('PUT', path, body, headers),
+    delete: (path: string, headers?: Record<string, string>) =>
+      makeRequest('DELETE', path, undefined, headers),
+    patch: (path: string, body?: any, headers?: Record<string, string>) =>
+      makeRequest('PATCH', path, body, headers)
+  };
+
+  async function makeRequest(
+    method: string,
+    path: string,
+    body?: any,
+    customHeaders?: Record<string, string>
+  ) {
+    const url = new URL(path, 'http://localhost:3000');
+
+    const headers: Record<string, string> = {
+      'Content-Type': 'application/json',
+      ...(setupAuth && { 'Authorization': 'Bearer test-token-123' }),
+      ...customHeaders
+    };
+
+    const request = new Request(url.toString(), {
+      method,
+      headers,
+      body: body ? JSON.stringify(body) : undefined,
+    });
+
+    try {
+      const response = await server.fetch(request);
+
+      const responseHeaders: Record<string, string> = {};
+      response.headers.forEach((value: string, key: string) => {
+        responseHeaders[key] = value;
+      });
+
+      let responseData: any;
+      const contentType = response.headers.get('content-type');
+
+      if (contentType?.includes('application/json')) {
+        responseData = await response.json();
+      } else {
+        responseData = await response.text();
+      }
+
+      return {
+        status: response.status,
+        headers: responseHeaders,
+        data: responseData,
+        json: async () => responseData,
+        text: async () => responseData
+      };
+    } catch (error) {
+      throw new Error(`Request failed: ${error}`);
+    }
+  }
+}
+
+/**
+ * 创建完整的测试应用实例
+ */
+export async function createTestApp(routes: any[]) {
+  const app = new OpenAPIHono();
+
+  // 注册所有路由
+  routes.forEach(route => {
+    if (typeof route === 'function') {
+      route(app);
+    }
+  });
+
+  return app;
+}
+
+/**
+ * 创建测试数据库连接
+ */
+export async function createTestDataSource(): Promise<DataSource> {
+  // 使用内存数据库或测试数据库
+  const dataSource = new DataSource({
+    type: 'better-sqlite3',
+    database: ':memory:',
+    synchronize: true,
+    logging: false,
+    entities: [], // 需要根据实际实体配置
+  });
+
+  await dataSource.initialize();
+  return dataSource;
+}

+ 52 - 0
src/server/api/__integration_tests__/basic.integration.test.ts

@@ -0,0 +1,52 @@
+import { describe, it, expect, vi } from 'vitest';
+import { OpenAPIHono } from '@hono/zod-openapi';
+import { createApiClient } from '../../__test_utils__/api-client';
+
+// 简单的mock测试
+describe('Basic API Integration Tests', () => {
+  it('应该创建测试服务器并响应请求', async () => {
+    // 创建简单的测试应用
+    const app = new OpenAPIHono();
+
+    // 添加一个简单的测试路由
+    app.get('/test', (c) => {
+      return c.json({ message: 'Hello, test!' });
+    });
+
+    // 创建API客户端
+    const apiClient = createApiClient(app);
+
+    // 发送请求
+    const response = await apiClient.get('/test');
+
+    // 验证响应
+    expect(response.status).toBe(200);
+    expect(response.data).toEqual({ message: 'Hello, test!' });
+  });
+
+  it('应该处理404错误', async () => {
+    const app = new OpenAPIHono();
+    const apiClient = createApiClient(app);
+
+    const response = await apiClient.get('/non-existent');
+
+    expect(response.status).toBe(404);
+  });
+
+  it('应该支持POST请求', async () => {
+    const app = new OpenAPIHono();
+
+    app.post('/echo', async (c) => {
+      const body = await c.req.json();
+      return c.json({ echoed: body });
+    });
+
+    const apiClient = createApiClient(app);
+    const testData = { name: 'test', value: 123 };
+
+    const response = await apiClient.post('/echo', testData);
+
+    expect(response.status).toBe(200);
+    expect(response.data).toEqual({ echoed: testData });
+  });
+});

+ 193 - 0
src/server/api/__integration_tests__/users.integration.test.ts

@@ -0,0 +1,193 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import { OpenAPIHono } from '@hono/zod-openapi';
+import { createApiClient, ApiClient } from '../../__test_utils__/api-client';
+import { createMockDataSource } from '../../__test_utils__/test-db';
+
+// Mock 数据源
+vi.mock('../../../data-source', () => {
+  const mockDataSource = createMockDataSource();
+  return {
+    AppDataSource: mockDataSource
+  };
+});
+
+// Mock 用户服务
+vi.mock('../../modules/users/user.service', () => ({
+  UserService: vi.fn().mockImplementation(() => ({
+    getUsersWithPagination: vi.fn().mockResolvedValue([[], 0]),
+    getUserById: vi.fn().mockResolvedValue(null),
+    createUser: vi.fn().mockResolvedValue({ id: 1, username: 'testuser' }),
+    updateUser: vi.fn().mockResolvedValue({ affected: 1 }),
+    deleteUser: vi.fn().mockResolvedValue({ affected: 1 })
+  }))
+}));
+
+// Mock 认证中间件
+vi.mock('../../middleware/auth.middleware', () => ({
+  authMiddleware: vi.fn().mockImplementation((_c, next) => next())
+}));
+
+describe('Users API Integration Tests', () => {
+  let app: OpenAPIHono;
+  let apiClient: ApiClient;
+  let mockDataSource: any;
+
+  beforeEach(async () => {
+    vi.clearAllMocks();
+
+    // 动态导入用户路由
+    const userRoutes = await import('../users/index');
+
+    // 使用导入的应用实例
+    app = userRoutes.default;
+
+    // 创建API客户端
+    apiClient = createApiClient(app, {
+      authToken: 'test-token-123'
+    });
+  });
+
+  afterEach(() => {
+    vi.resetAllMocks();
+  });
+
+  describe('GET /users', () => {
+    it('应该返回用户列表和分页信息', async () => {
+      // 模拟用户服务返回数据
+      const mockUserService = require('../../modules/users/user.service').UserService();
+      const mockUsers = [
+        { id: 1, username: 'user1', email: 'user1@example.com' },
+        { id: 2, username: 'user2', email: 'user2@example.com' }
+      ];
+      mockUserService.getUsersWithPagination.mockResolvedValue([mockUsers, 2]);
+
+      const response = await apiClient.get('/users?page=1&pageSize=10');
+
+      expect(response.status).toBe(200);
+      expect(response.data).toEqual({
+        data: mockUsers,
+        pagination: {
+          total: 2,
+          current: 1,
+          pageSize: 10
+        }
+      });
+    });
+
+    it('应该验证分页参数', async () => {
+      const response = await apiClient.get('/users?page=0&pageSize=0');
+
+      expect(response.status).toBe(400);
+      expect(response.data).toMatchObject({
+        success: false,
+        error: expect.any(Object)
+      });
+    });
+
+    it('应该支持关键词搜索', async () => {
+      const mockUserService = require('../../modules/users/user.service').UserService();
+      mockUserService.getUsersWithPagination.mockResolvedValue([[], 0]);
+
+      const response = await apiClient.get('/users?page=1&pageSize=10&keyword=admin');
+
+      expect(response.status).toBe(200);
+      expect(mockUserService.getUsersWithPagination).toHaveBeenCalledWith({
+        page: 1,
+        pageSize: 10,
+        keyword: 'admin'
+      });
+    });
+  });
+
+  describe('GET /users/:id', () => {
+    it('应该返回特定用户信息', async () => {
+      const mockUser = { id: 1, username: 'testuser', email: 'test@example.com' };
+      const mockUserService = require('../../modules/users/user.service').UserService();
+      mockUserService.getUserById.mockResolvedValue(mockUser);
+
+      const response = await apiClient.get('/users/1');
+
+      expect(response.status).toBe(200);
+      expect(response.data).toEqual(mockUser);
+      expect(mockUserService.getUserById).toHaveBeenCalledWith(1);
+    });
+
+    it('应该在用户不存在时返回404', async () => {
+      const mockUserService = require('../../modules/users/user.service').UserService();
+      mockUserService.getUserById.mockResolvedValue(null);
+
+      const response = await apiClient.get('/users/999');
+
+      expect(response.status).toBe(404);
+      expect(response.data).toMatchObject({
+        code: 404,
+        message: expect.any(String)
+      });
+    });
+
+    it('应该验证用户ID格式', async () => {
+      const response = await apiClient.get('/users/invalid');
+
+      expect(response.status).toBe(400);
+      expect(response.data).toMatchObject({
+        success: false,
+        error: expect.any(Object)
+      });
+    });
+  });
+
+  describe('错误处理', () => {
+    it('应该在服务错误时返回500状态码', async () => {
+      const mockUserService = require('../../modules/users/user.service').UserService();
+      mockUserService.getUsersWithPagination.mockRejectedValue(new Error('Database error'));
+
+      const response = await apiClient.get('/users?page=1&pageSize=10');
+
+      expect(response.status).toBe(500);
+      expect(response.data).toMatchObject({
+        code: 500,
+        message: 'Database error'
+      });
+    });
+
+    it('应该在未知错误时返回通用错误消息', async () => {
+      const mockUserService = require('../../modules/users/user.service').UserService();
+      mockUserService.getUsersWithPagination.mockRejectedValue('Unknown error');
+
+      const response = await apiClient.get('/users?page=1&pageSize=10');
+
+      expect(response.status).toBe(500);
+      expect(response.data).toMatchObject({
+        code: 500,
+        message: '获取用户列表失败'
+      });
+    });
+  });
+
+  describe('认证和授权', () => {
+    it('应该在缺少认证令牌时返回401', async () => {
+      apiClient.clearAuthToken();
+
+      const response = await apiClient.get('/users');
+
+      expect(response.status).toBe(401);
+      expect(response.data).toMatchObject({
+        code: 401,
+        message: expect.any(String)
+      });
+    });
+
+    it('应该在无效令牌时返回401', async () => {
+      // 模拟认证中间件验证失败
+      const authMiddleware = require('../../middleware/auth.middleware').authMiddleware;
+      authMiddleware.mockImplementation((c, next) => {
+        return c.json({ error: 'Invalid token' }, 401);
+      });
+
+      const response = await apiClient.get('/users');
+
+      expect(response.status).toBe(401);
+      expect(response.data).toEqual({ error: 'Invalid token' });
+    });
+  });
+});

+ 68 - 0
vitest.config.components.ts

@@ -0,0 +1,68 @@
+import { defineConfig } from 'vitest/config'
+import { resolve } from 'path'
+
+export default defineConfig({
+  test: {
+    // 测试环境 - 使用happy-dom进行组件测试
+    environment: 'happy-dom',
+
+    // 测试文件匹配模式
+    include: [
+      'src/client/__integration_tests__/**/*.test.{js,ts,jsx,tsx}',
+      'src/client/__tests__/**/*.test.{js,ts,jsx,tsx}'
+    ],
+
+    // 排除模式
+    exclude: [
+      '**/node_modules/**',
+      '**/dist/**',
+      '**/build/**',
+      '**/coverage/**',
+      'src/server/**'
+    ],
+
+    // 覆盖率配置
+    coverage: {
+      provider: 'v8',
+      reporter: ['text', 'lcov', 'html'],
+      reportsDirectory: './coverage/components',
+      exclude: [
+        '**/node_modules/**',
+        '**/dist/**',
+        '**/build/**',
+        '**/coverage/**',
+        '**/*.d.ts',
+        'src/client/api.ts',
+        '**/__tests__/**',
+        '**/__mocks__/**',
+        '**/index.ts',
+        '**/types.ts',
+        'src/server/**'
+      ],
+      thresholds: {
+        branches: 60,
+        functions: 60,
+        lines: 60,
+        statements: 60
+      }
+    },
+
+    // 全局设置
+    globals: true,
+
+    // 测试超时
+    testTimeout: 10000,
+
+    // 设置文件
+    setupFiles: ['./src/test/setup.ts'],
+
+    // 别名配置
+    alias: {
+      '@': resolve(__dirname, './src'),
+      '@/client': resolve(__dirname, './src/client'),
+      '@/server': resolve(__dirname, './src/server'),
+      '@/share': resolve(__dirname, './src/share'),
+      '@/test': resolve(__dirname, './test')
+    }
+  }
+})