ソースを参照

docs(architecture): 修正UI包规范文档中的测试描述

- 修正测试文件结构:从__tests__/改为tests/,与实际UI包一致
- 添加标准的createMockResponse工具函数
- 更新组件集成测试示例,使用实际的mock模式(areaClientManager和areaClient)
- 添加Hook单元测试和组件单元测试示例
- 移除不正确的MSW集成测试,改为直接mock客户端

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 5 日 前
コミット
00240aa1b7
1 ファイル変更199 行追加46 行削除
  1. 199 46
      docs/architecture/ui-package-standards.md

+ 199 - 46
docs/architecture/ui-package-standards.md

@@ -316,32 +316,107 @@ const mutation = useMutation({
 
 ## 测试规范
 
-### 单元测试
+### 测试文件结构
+```text
+packages/<module-name>-ui/
+├── tests/
+│   ├── integration/                    # 集成测试
+│   │   └── <component-name>.integration.test.tsx
+│   ├── unit/                          # 单元测试
+│   │   └── <hook-name>.test.tsx
+│   └── components/                    # 组件测试
+│       └── <ComponentName>.test.tsx
+```
+
+### Mock响应工具函数
 ```typescript
-// __tests__/<ComponentName>.test.tsx
+// 在测试文件中使用的标准mock响应函数
+const createMockResponse = (status: number, data?: any) => ({
+  status,
+  ok: status >= 200 && status < 300,
+  body: null,
+  bodyUsed: false,
+  statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
+  headers: new Headers(),
+  url: '',
+  redirected: false,
+  type: 'basic' as ResponseType,
+  json: async () => data || {},
+  text: async () => '',
+  blob: async () => new Blob(),
+  arrayBuffer: async () => new ArrayBuffer(0),
+  formData: async () => new FormData(),
+  clone: function() { return this; }
+});
+```
+
+### 组件集成测试
+```typescript
+// tests/integration/<component-name>.integration.test.tsx
 import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { render, screen, fireEvent, waitFor } from '@testing-library/react';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { <ComponentName> } from '../src/components/<ComponentName>';
-import { <module>ClientManager } from '../src/api/<module>Client';
+import { <ComponentName> } from '../../src/components/<ComponentName>';
+import { <module>ClientManager, <module>Client } from '../../src/api/<module>Client';
 
 // Mock RPC客户端
-vi.mock('../src/api/<module>Client', () => ({
-  <module>ClientManager: {
-    getInstance: vi.fn(() => ({
-      get: vi.fn(() => ({
-        index: {
-          $get: vi.fn(() => Promise.resolve({
-            status: 200,
-            json: () => Promise.resolve({ data: [], pagination: { total: 0 } })
-          }))
+vi.mock('../../src/api/<module>Client', () => {
+  const mock<Module>Client = {
+    index: {
+      $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
+        data: [
+          {
+            id: 1,
+            name: '测试数据',
+            // 其他字段
+          }
+        ],
+        pagination: {
+          page: 1,
+          pageSize: 10,
+          total: 1,
+          totalPages: 1
         }
-      }))
-    }))
+      }))),
+      $post: vi.fn(() => Promise.resolve(createMockResponse(201, {
+        id: 2,
+        name: '新建数据'
+      }))),
+    },
+    ':id': {
+      $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
+        id: 1,
+        name: '测试数据详情'
+      }))),
+      $put: vi.fn(() => Promise.resolve(createMockResponse(200, {
+        id: 1,
+        name: '更新后的数据'
+      }))),
+      $delete: vi.fn(() => Promise.resolve(createMockResponse(204))),
+    },
+  };
+
+  const mock<Module>ClientManager = {
+    get: vi.fn(() => mock<Module>Client),
+  };
+
+  return {
+    <module>ClientManager: mock<Module>ClientManager,
+    <module>Client: mock<Module>Client,
+  };
+});
+
+// Mock其他依赖
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(),
+    error: vi.fn(),
+    info: vi.fn(),
+    warning: vi.fn(),
   }
 }));
 
-describe('<ComponentName>', () => {
+describe('<ComponentName>集成测试', () => {
   const queryClient = new QueryClient({
     defaultOptions: {
       queries: {
@@ -350,54 +425,132 @@ describe('<ComponentName>', () => {
     },
   });
 
-  it('渲染组件', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    queryClient.clear();
+  });
+
+  it('渲染组件并加载数据', async () => {
     render(
       <QueryClientProvider client={queryClient}>
         <<ComponentName> />
       </QueryClientProvider>
     );
-    expect(screen.getByText('组件标题')).toBeInTheDocument();
+
+    await waitFor(() => {
+      expect(screen.getByText('测试数据')).toBeInTheDocument();
+    });
+  });
+
+  it('创建新数据', async () => {
+    render(
+      <QueryClientProvider client={queryClient}>
+        <<ComponentName> />
+      </QueryClientProvider>
+    );
+
+    const createButton = screen.getByText('新建');
+    fireEvent.click(createButton);
+
+    const nameInput = screen.getByLabelText('名称');
+    fireEvent.change(nameInput, { target: { value: '新数据' } });
+
+    const submitButton = screen.getByText('提交');
+    fireEvent.click(submitButton);
+
+    await waitFor(() => {
+      expect(<module>Client.index.$post).toHaveBeenCalledWith({
+        json: expect.objectContaining({ name: '新数据' })
+      });
+    });
   });
 });
 ```
 
-### 集成测试
+### Hook单元测试
 ```typescript
-// __tests__/integration/<ComponentName>.integration.test.tsx
-import { describe, it, expect, beforeAll, afterAll } from 'vitest';
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+// tests/unit/use<HookName>.test.tsx
+import React from 'react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { renderHook, waitFor } from '@testing-library/react';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { http, HttpResponse } from 'msw';
-import { setupServer } from 'msw/node';
-import { <ComponentName> } from '../../src/components/<ComponentName>';
+import { use<HookName> } from '../../src/hooks/use<HookName>';
+import { <module>Client } from '../../src/api/<module>Client';
 
-// 设置Mock Server
-const server = setupServer(
-  http.get('/api/<module>', () => {
-    return HttpResponse.json({
-      data: [{ id: 1, name: '测试数据' }],
-      pagination: { total: 1 }
-    });
-  })
-);
+// Mock RPC客户端
+vi.mock('../../src/api/<module>Client', () => {
+  const mock<Module>Client = {
+    index: {
+      $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
+        data: [{ id: 1, name: '测试数据' }],
+        pagination: { total: 1 }
+      }))),
+    },
+  };
 
-describe('<ComponentName>集成测试', () => {
-  beforeAll(() => server.listen());
-  afterAll(() => server.close());
-  afterEach(() => server.resetHandlers());
+  return {
+    <module>Client: mock<Module>Client,
+  };
+});
 
-  it('从API加载数据并显示', async () => {
-    const queryClient = new QueryClient();
+describe('use<HookName>', () => {
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+      },
+    },
+  });
 
-    render(
-      <QueryClientProvider client={queryClient}>
-        <<ComponentName> />
-      </QueryClientProvider>
-    );
+  const wrapper = ({ children }: { children: React.ReactNode }) => (
+    <QueryClientProvider client={queryClient}>
+      {children}
+    </QueryClientProvider>
+  );
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+    queryClient.clear();
+  });
+
+  it('加载数据', async () => {
+    const { result } = renderHook(() => use<HookName>({ page: 1, limit: 10 }), { wrapper });
 
     await waitFor(() => {
-      expect(screen.getByText('测试数据')).toBeInTheDocument();
+      expect(result.current.isLoading).toBe(false);
     });
+
+    expect(result.current.data).toEqual([
+      { id: 1, name: '测试数据' }
+    ]);
+    expect(<module>Client.index.$get).toHaveBeenCalledWith({
+      query: { page: 1, pageSize: 10 }
+    });
+  });
+});
+```
+
+### 组件单元测试
+```typescript
+// tests/components/<ComponentName>.test.tsx
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { <ComponentName> } from '../../src/components/<ComponentName>';
+
+// Mock子组件
+vi.mock('../ChildComponent', () => ({
+  ChildComponent: () => <div>Mock子组件</div>
+}));
+
+describe('<ComponentName>', () => {
+  it('渲染组件', () => {
+    render(<<ComponentName> />);
+    expect(screen.getByText('组件标题')).toBeInTheDocument();
+  });
+
+  it('显示传入的属性', () => {
+    render(<<ComponentName> name="测试名称" />);
+    expect(screen.getByText('测试名称')).toBeInTheDocument();
   });
 });
 ```