|
|
@@ -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();
|
|
|
});
|
|
|
});
|
|
|
```
|