测试指南
本指南介绍如何在 Trae 应用程序中实施全面的测试策略,包括单元测试、集成测试、端到端测试等。
概述
测试是确保应用程序质量和可靠性的关键环节。Trae 支持多种测试框架和工具,帮助您构建健壮的测试套件。本指南将涵盖测试策略、工具选择、最佳实践等方面。
测试策略
测试金字塔
/\ E2E Tests (少量)
/ \
/____\ Integration Tests (适量)
/______\ Unit Tests (大量)- 单元测试 (70%):测试单个函数或组件
- 集成测试 (20%):测试组件间的交互
- 端到端测试 (10%):测试完整的用户流程
测试配置
javascript
// jest.config.js
module.exports = {
// 测试环境
testEnvironment: 'jsdom',
// 设置文件
setupFilesAfterEnv: ['<rootDir>/src/test/setup.js'],
// 模块路径映射
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
'^@components/(.*)$': '<rootDir>/src/components/$1',
'^@utils/(.*)$': '<rootDir>/src/utils/$1',
'^@services/(.*)$': '<rootDir>/src/services/$1'
},
// 测试文件匹配模式
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{test,spec}.{js,jsx,ts,tsx}'
],
// 覆盖率配置
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js',
'!src/serviceWorker.js',
'!src/**/*.stories.{js,jsx,ts,tsx}'
],
// 覆盖率阈值
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
// 转换配置
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
'^.+\\.css$': 'jest-transform-css',
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': 'jest-transform-file'
},
// 模拟文件
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
// 忽略的转换路径
transformIgnorePatterns: [
'[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$',
'^.+\\.module\\.(css|sass|scss)$'
],
// 模拟模块
moduleNameMapping: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
}
};测试设置
javascript
// src/test/setup.js
import '@testing-library/jest-dom';
import { configure } from '@testing-library/react';
import { server } from './mocks/server';
// 配置 Testing Library
configure({ testIdAttribute: 'data-testid' });
// 设置 MSW
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// 全局模拟
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn()
}));
global.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn()
}));
// 模拟 localStorage
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn()
};
global.localStorage = localStorageMock;
// 模拟 fetch
global.fetch = jest.fn();
// 清理函数
afterEach(() => {
jest.clearAllMocks();
localStorageMock.clear();
});单元测试
组件测试
javascript
// src/components/Button/__tests__/Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from '../Button';
describe('Button Component', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('handles click events', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('applies correct variant styles', () => {
const { rerender } = render(<Button variant="primary">Primary</Button>);
expect(screen.getByRole('button')).toHaveClass('btn-primary');
rerender(<Button variant="secondary">Secondary</Button>);
expect(screen.getByRole('button')).toHaveClass('btn-secondary');
});
it('disables button when loading', () => {
render(<Button loading>Loading</Button>);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
it('renders with custom className', () => {
render(<Button className="custom-class">Button</Button>);
expect(screen.getByRole('button')).toHaveClass('custom-class');
});
it('forwards ref correctly', () => {
const ref = React.createRef();
render(<Button ref={ref}>Button</Button>);
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
});
});Hook 测试
javascript
// src/hooks/__tests__/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from '../useCounter';
describe('useCounter Hook', () => {
it('initializes with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('initializes with custom value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('increments count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('decrements count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
it('resets count', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(10);
});
it('sets count to specific value', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.setValue(42);
});
expect(result.current.count).toBe(42);
});
});工具函数测试
javascript
// src/utils/__tests__/formatters.test.js
import {
formatCurrency,
formatDate,
formatFileSize,
truncateText
} from '../formatters';
describe('Formatter Utils', () => {
describe('formatCurrency', () => {
it('formats positive numbers correctly', () => {
expect(formatCurrency(1234.56)).toBe('$1,234.56');
expect(formatCurrency(0)).toBe('$0.00');
expect(formatCurrency(999999.99)).toBe('$999,999.99');
});
it('formats negative numbers correctly', () => {
expect(formatCurrency(-1234.56)).toBe('-$1,234.56');
});
it('handles different currencies', () => {
expect(formatCurrency(1234.56, 'EUR')).toBe('€1,234.56');
expect(formatCurrency(1234.56, 'JPY')).toBe('¥1,235');
});
it('throws error for invalid input', () => {
expect(() => formatCurrency('invalid')).toThrow();
expect(() => formatCurrency(null)).toThrow();
});
});
describe('formatDate', () => {
it('formats dates correctly', () => {
const date = new Date('2023-12-25T10:30:00Z');
expect(formatDate(date)).toBe('2023-12-25');
expect(formatDate(date, 'MM/dd/yyyy')).toBe('12/25/2023');
});
it('handles string dates', () => {
expect(formatDate('2023-12-25')).toBe('2023-12-25');
});
it('throws error for invalid dates', () => {
expect(() => formatDate('invalid-date')).toThrow();
});
});
describe('formatFileSize', () => {
it('formats bytes correctly', () => {
expect(formatFileSize(0)).toBe('0 B');
expect(formatFileSize(1024)).toBe('1 KB');
expect(formatFileSize(1048576)).toBe('1 MB');
expect(formatFileSize(1073741824)).toBe('1 GB');
});
it('handles decimal places', () => {
expect(formatFileSize(1536, 1)).toBe('1.5 KB');
expect(formatFileSize(1536, 2)).toBe('1.50 KB');
});
});
describe('truncateText', () => {
it('truncates long text', () => {
const longText = 'This is a very long text that should be truncated';
expect(truncateText(longText, 20)).toBe('This is a very long...');
});
it('returns original text if shorter than limit', () => {
const shortText = 'Short text';
expect(truncateText(shortText, 20)).toBe('Short text');
});
it('handles custom suffix', () => {
const text = 'This is a long text';
expect(truncateText(text, 10, ' [more]')).toBe('This is a [more]');
});
});
});集成测试
API 集成测试
javascript
// src/services/__tests__/userService.test.js
import { rest } from 'msw';
import { server } from '../../test/mocks/server';
import userService from '../userService';
describe('User Service', () => {
describe('getUsers', () => {
it('fetches users successfully', async () => {
const mockUsers = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
server.use(
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.json(mockUsers));
})
);
const users = await userService.getUsers();
expect(users).toEqual(mockUsers);
});
it('handles API errors', async () => {
server.use(
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
})
);
await expect(userService.getUsers()).rejects.toThrow('Failed to fetch users');
});
it('handles network errors', async () => {
server.use(
rest.get('/api/users', (req, res, ctx) => {
return res.networkError('Network error');
})
);
await expect(userService.getUsers()).rejects.toThrow('Network error');
});
});
describe('createUser', () => {
it('creates user successfully', async () => {
const newUser = { name: 'New User', email: 'new@example.com' };
const createdUser = { id: 3, ...newUser };
server.use(
rest.post('/api/users', async (req, res, ctx) => {
const body = await req.json();
expect(body).toEqual(newUser);
return res(ctx.status(201), ctx.json(createdUser));
})
);
const result = await userService.createUser(newUser);
expect(result).toEqual(createdUser);
});
it('handles validation errors', async () => {
server.use(
rest.post('/api/users', (req, res, ctx) => {
return res(
ctx.status(400),
ctx.json({
error: 'Validation failed',
details: { email: 'Email is required' }
})
);
})
);
await expect(userService.createUser({})).rejects.toThrow('Validation failed');
});
});
});组件集成测试
javascript
// src/components/UserList/__tests__/UserList.integration.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { server } from '../../../test/mocks/server';
import UserList from '../UserList';
import { AuthProvider } from '../../../contexts/AuthContext';
import { QueryClient, QueryClientProvider } from 'react-query';
const renderWithProviders = (component) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false }
}
});
return render(
<QueryClientProvider client={queryClient}>
<AuthProvider>
{component}
</AuthProvider>
</QueryClientProvider>
);
};
describe('UserList Integration', () => {
const mockUsers = [
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'admin' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'user' }
];
beforeEach(() => {
server.use(
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.json(mockUsers));
})
);
});
it('loads and displays users', async () => {
renderWithProviders(<UserList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
});
});
it('handles search functionality', async () => {
const user = userEvent.setup();
renderWithProviders(<UserList />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
const searchInput = screen.getByPlaceholderText(/search users/i);
await user.type(searchInput, 'John');
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.queryByText('Jane Smith')).not.toBeInTheDocument();
});
it('handles user deletion', async () => {
const user = userEvent.setup();
server.use(
rest.delete('/api/users/:id', (req, res, ctx) => {
return res(ctx.status(204));
}),
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.json(mockUsers.filter(u => u.id !== 1)));
})
);
renderWithProviders(<UserList />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
const deleteButton = screen.getAllByText(/delete/i)[0];
await user.click(deleteButton);
const confirmButton = screen.getByText(/confirm/i);
await user.click(confirmButton);
await waitFor(() => {
expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
});
});
it('handles API errors gracefully', async () => {
server.use(
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Server error' }));
})
);
renderWithProviders(<UserList />);
await waitFor(() => {
expect(screen.getByText(/error loading users/i)).toBeInTheDocument();
});
});
});端到端测试
Playwright 配置
javascript
// playwright.config.js
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] }
}
],
webServer: {
command: 'npm run start',
port: 3000,
reuseExistingServer: !process.env.CI
}
});E2E 测试示例
javascript
// e2e/auth.spec.js
import { test, expect } from '@playwright/test';
test.describe('Authentication Flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('user can login successfully', async ({ page }) => {
// 导航到登录页面
await page.click('text=Login');
await expect(page).toHaveURL('/login');
// 填写登录表单
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
// 提交表单
await page.click('[data-testid="login-button"]');
// 验证登录成功
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
await expect(page.locator('text=Welcome back')).toBeVisible();
});
test('shows error for invalid credentials', async ({ page }) => {
await page.click('text=Login');
await page.fill('[data-testid="email-input"]', 'invalid@example.com');
await page.fill('[data-testid="password-input"]', 'wrongpassword');
await page.click('[data-testid="login-button"]');
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('text=Invalid credentials')).toBeVisible();
});
test('user can logout', async ({ page }) => {
// 先登录
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/dashboard');
// 登出
await page.click('[data-testid="user-menu"]');
await page.click('text=Logout');
// 验证登出成功
await expect(page).toHaveURL('/');
await expect(page.locator('text=Login')).toBeVisible();
});
test('redirects to login when accessing protected route', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveURL('/login');
});
});复杂用户流程测试
javascript
// e2e/project-management.spec.js
import { test, expect } from '@playwright/test';
test.describe('Project Management', () => {
test.beforeEach(async ({ page }) => {
// 登录
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'admin@example.com');
await page.fill('[data-testid="password-input"]', 'admin123');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/dashboard');
});
test('complete project creation flow', async ({ page }) => {
// 导航到项目页面
await page.click('text=Projects');
await expect(page).toHaveURL('/projects');
// 创建新项目
await page.click('[data-testid="create-project-button"]');
// 填写项目信息
await page.fill('[data-testid="project-name"]', 'Test Project');
await page.fill('[data-testid="project-description"]', 'This is a test project');
await page.selectOption('[data-testid="project-template"]', 'react');
// 配置项目设置
await page.check('[data-testid="enable-typescript"]');
await page.check('[data-testid="enable-testing"]');
// 提交创建
await page.click('[data-testid="create-button"]');
// 等待项目创建完成
await expect(page.locator('[data-testid="project-status"]')).toHaveText('Ready', { timeout: 30000 });
// 验证项目详情
await expect(page.locator('[data-testid="project-title"]')).toHaveText('Test Project');
await expect(page.locator('[data-testid="project-description"]')).toHaveText('This is a test project');
// 验证文件结构
await expect(page.locator('[data-testid="file-tree"]')).toBeVisible();
await expect(page.locator('text=src/')).toBeVisible();
await expect(page.locator('text=package.json')).toBeVisible();
await expect(page.locator('text=tsconfig.json')).toBeVisible();
});
test('file editing and saving', async ({ page }) => {
// 打开现有项目
await page.goto('/projects/test-project');
// 打开文件
await page.click('text=src/');
await page.click('text=App.tsx');
// 等待编辑器加载
await expect(page.locator('[data-testid="code-editor"]')).toBeVisible();
// 编辑代码
await page.click('[data-testid="code-editor"]');
await page.keyboard.press('Control+A');
await page.keyboard.type(`
function App() {
return (
<div className="App">
<h1>Hello, Trae!</h1>
</div>
);
}
export default App;
`);
// 保存文件
await page.keyboard.press('Control+S');
// 验证保存状态
await expect(page.locator('[data-testid="save-indicator"]')).toHaveText('Saved');
// 运行项目
await page.click('[data-testid="run-button"]');
// 等待预览加载
await expect(page.locator('[data-testid="preview-frame"]')).toBeVisible({ timeout: 10000 });
// 验证预览内容
const previewFrame = page.frameLocator('[data-testid="preview-frame"]');
await expect(previewFrame.locator('h1')).toHaveText('Hello, Trae!');
});
test('collaboration features', async ({ page, context }) => {
// 打开项目
await page.goto('/projects/shared-project');
// 邀请协作者
await page.click('[data-testid="share-button"]');
await page.fill('[data-testid="invite-email"]', 'collaborator@example.com');
await page.selectOption('[data-testid="permission-level"]', 'editor');
await page.click('[data-testid="send-invite"]');
// 验证邀请发送
await expect(page.locator('text=Invitation sent')).toBeVisible();
// 模拟协作者加入
const collaboratorPage = await context.newPage();
await collaboratorPage.goto('/login');
await collaboratorPage.fill('[data-testid="email-input"]', 'collaborator@example.com');
await collaboratorPage.fill('[data-testid="password-input"]', 'password123');
await collaboratorPage.click('[data-testid="login-button"]');
await collaboratorPage.goto('/projects/shared-project');
// 验证实时协作
await page.click('text=src/App.tsx');
await collaboratorPage.click('text=src/App.tsx');
// 在主页面编辑
await page.click('[data-testid="code-editor"]');
await page.keyboard.type('// Main user edit\n');
// 验证协作者页面显示更改
await expect(collaboratorPage.locator('text=// Main user edit')).toBeVisible({ timeout: 5000 });
// 在协作者页面编辑
await collaboratorPage.click('[data-testid="code-editor"]');
await collaboratorPage.keyboard.type('// Collaborator edit\n');
// 验证主页面显示更改
await expect(page.locator('text=// Collaborator edit')).toBeVisible({ timeout: 5000 });
});
});测试工具和辅助函数
测试工具库
javascript
// src/test/utils/testUtils.jsx
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from '../../contexts/AuthContext';
import { ThemeProvider } from '../../contexts/ThemeContext';
// 创建测试查询客户端
const createTestQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: 0
},
mutations: {
retry: false
}
}
});
// 全功能渲染函数
export function renderWithProviders(
ui,
{
initialEntries = ['/'],
user = null,
queryClient = createTestQueryClient(),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AuthProvider initialUser={user}>
<ThemeProvider>
{children}
</ThemeProvider>
</AuthProvider>
</BrowserRouter>
</QueryClientProvider>
);
}
return {
...render(ui, { wrapper: Wrapper, ...renderOptions }),
queryClient
};
}
// 模拟用户数据
export const mockUser = {
id: 1,
name: 'Test User',
email: 'test@example.com',
roles: ['user'],
permissions: ['read:projects']
};
export const mockAdminUser = {
id: 2,
name: 'Admin User',
email: 'admin@example.com',
roles: ['admin'],
permissions: ['read:projects', 'create:projects', 'update:projects', 'delete:projects']
};
// 等待异步操作完成
export const waitForLoadingToFinish = () => {
return waitFor(() => {
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
};
// 模拟文件上传
export const createMockFile = (name = 'test.txt', size = 1024, type = 'text/plain') => {
const file = new File(['test content'], name, { type });
Object.defineProperty(file, 'size', { value: size });
return file;
};
// 模拟拖拽事件
export const createDragEvent = (type, files = []) => {
const event = new Event(type, { bubbles: true });
Object.defineProperty(event, 'dataTransfer', {
value: {
files,
types: ['Files']
}
});
return event;
};
// 表单测试辅助函数
export const fillForm = async (fields) => {
const user = userEvent.setup();
for (const [fieldName, value] of Object.entries(fields)) {
const field = screen.getByLabelText(new RegExp(fieldName, 'i'));
if (field.type === 'checkbox' || field.type === 'radio') {
if (value) {
await user.click(field);
}
} else if (field.tagName === 'SELECT') {
await user.selectOptions(field, value);
} else {
await user.clear(field);
await user.type(field, value);
}
}
};
// 等待元素出现
export const waitForElement = (selector, options = {}) => {
return waitFor(() => {
const element = screen.getByTestId(selector);
expect(element).toBeInTheDocument();
return element;
}, options);
};Mock 服务
javascript
// src/test/mocks/handlers.js
import { rest } from 'msw';
const baseURL = process.env.REACT_APP_API_URL || 'http://localhost:3001';
export const handlers = [
// 认证相关
rest.post(`${baseURL}/auth/login`, (req, res, ctx) => {
const { email, password } = req.body;
if (email === 'test@example.com' && password === 'password123') {
return res(
ctx.json({
user: {
id: 1,
name: 'Test User',
email: 'test@example.com',
roles: ['user']
},
accessToken: 'mock-access-token',
refreshToken: 'mock-refresh-token'
})
);
}
return res(
ctx.status(401),
ctx.json({ error: 'Invalid credentials' })
);
}),
rest.post(`${baseURL}/auth/refresh`, (req, res, ctx) => {
return res(
ctx.json({
accessToken: 'new-mock-access-token'
})
);
}),
rest.post(`${baseURL}/auth/logout`, (req, res, ctx) => {
return res(ctx.status(204));
}),
// 用户管理
rest.get(`${baseURL}/users`, (req, res, ctx) => {
return res(
ctx.json([
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'admin' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'user' }
])
);
}),
rest.post(`${baseURL}/users`, (req, res, ctx) => {
const user = req.body;
return res(
ctx.status(201),
ctx.json({ id: Date.now(), ...user })
);
}),
rest.put(`${baseURL}/users/:id`, (req, res, ctx) => {
const { id } = req.params;
const updates = req.body;
return res(
ctx.json({ id: parseInt(id), ...updates })
);
}),
rest.delete(`${baseURL}/users/:id`, (req, res, ctx) => {
return res(ctx.status(204));
}),
// 项目管理
rest.get(`${baseURL}/projects`, (req, res, ctx) => {
return res(
ctx.json([
{
id: 1,
name: 'Test Project',
description: 'A test project',
status: 'active',
createdAt: '2023-01-01T00:00:00Z'
}
])
);
}),
rest.post(`${baseURL}/projects`, (req, res, ctx) => {
const project = req.body;
return res(
ctx.status(201),
ctx.json({
id: Date.now(),
...project,
status: 'creating',
createdAt: new Date().toISOString()
})
);
}),
// 文件操作
rest.get(`${baseURL}/projects/:id/files`, (req, res, ctx) => {
return res(
ctx.json({
files: [
{ name: 'src/', type: 'directory' },
{ name: 'package.json', type: 'file' },
{ name: 'README.md', type: 'file' }
]
})
);
}),
rest.get(`${baseURL}/projects/:id/files/*`, (req, res, ctx) => {
return res(
ctx.text('// File content')
);
}),
rest.put(`${baseURL}/projects/:id/files/*`, (req, res, ctx) => {
return res(ctx.status(204));
})
];性能测试
组件性能测试
javascript
// src/components/__tests__/performance.test.jsx
import { render } from '@testing-library/react';
import { performance } from 'perf_hooks';
import LargeList from '../LargeList';
describe('Performance Tests', () => {
const generateLargeDataset = (size) => {
return Array.from({ length: size }, (_, index) => ({
id: index,
name: `Item ${index}`,
description: `Description for item ${index}`,
value: Math.random() * 1000
}));
};
it('renders large list efficiently', () => {
const data = generateLargeDataset(10000);
const startTime = performance.now();
render(<LargeList items={data} />);
const endTime = performance.now();
const renderTime = endTime - startTime;
// 渲染时间应该少于 100ms
expect(renderTime).toBeLessThan(100);
});
it('handles frequent updates efficiently', async () => {
const data = generateLargeDataset(1000);
const { rerender } = render(<LargeList items={data} />);
const startTime = performance.now();
// 模拟频繁更新
for (let i = 0; i < 100; i++) {
const updatedData = data.map(item => ({
...item,
value: Math.random() * 1000
}));
rerender(<LargeList items={updatedData} />);
}
const endTime = performance.now();
const updateTime = endTime - startTime;
// 100次更新应该在 500ms 内完成
expect(updateTime).toBeLessThan(500);
});
it('memory usage stays within bounds', () => {
const initialMemory = process.memoryUsage().heapUsed;
const data = generateLargeDataset(5000);
const { unmount } = render(<LargeList items={data} />);
const afterRenderMemory = process.memoryUsage().heapUsed;
const memoryIncrease = afterRenderMemory - initialMemory;
unmount();
// 强制垃圾回收(如果可用)
if (global.gc) {
global.gc();
}
const afterUnmountMemory = process.memoryUsage().heapUsed;
// 内存增长应该合理(小于 50MB)
expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024);
// 卸载后内存应该释放大部分
const memoryLeakage = afterUnmountMemory - initialMemory;
expect(memoryLeakage).toBeLessThan(memoryIncrease * 0.1);
});
});最佳实践
测试策略
- 测试金字塔:70% 单元测试,20% 集成测试,10% E2E 测试
- 测试驱动开发:先写测试,再写实现
- 行为驱动测试:测试用户行为而不是实现细节
- 快速反馈:保持测试快速运行
- 可维护性:编写清晰、可读的测试代码
测试命名规范
javascript
// 好的测试命名
describe('UserService', () => {
describe('when user is authenticated', () => {
it('should return user profile', () => {});
it('should handle API errors gracefully', () => {});
});
describe('when user is not authenticated', () => {
it('should redirect to login page', () => {});
it('should clear user data', () => {});
});
});
// 使用 Given-When-Then 模式
it('should display error message when login fails', async () => {
// Given: 用户在登录页面
render(<LoginPage />);
// When: 用户输入错误的凭据并提交
await userEvent.type(screen.getByLabelText(/email/i), 'wrong@email.com');
await userEvent.type(screen.getByLabelText(/password/i), 'wrongpassword');
await userEvent.click(screen.getByRole('button', { name: /login/i }));
// Then: 应该显示错误消息
await expect(screen.findByText(/invalid credentials/i)).resolves.toBeInTheDocument();
});测试数据管理
javascript
// src/test/fixtures/index.js
export const userFixtures = {
regularUser: {
id: 1,
name: 'John Doe',
email: 'john@example.com',
roles: ['user'],
permissions: ['read:projects']
},
adminUser: {
id: 2,
name: 'Admin User',
email: 'admin@example.com',
roles: ['admin'],
permissions: ['read:projects', 'create:projects', 'update:projects', 'delete:projects']
}
};
export const projectFixtures = {
activeProject: {
id: 1,
name: 'Active Project',
description: 'An active project',
status: 'active',
owner: userFixtures.regularUser,
createdAt: '2023-01-01T00:00:00Z'
},
archivedProject: {
id: 2,
name: 'Archived Project',
description: 'An archived project',
status: 'archived',
owner: userFixtures.adminUser,
createdAt: '2022-01-01T00:00:00Z'
}
};
// 工厂函数
export const createUser = (overrides = {}) => ({
...userFixtures.regularUser,
...overrides
});
export const createProject = (overrides = {}) => ({
...projectFixtures.activeProject,
...overrides
});通过遵循这些测试指南和最佳实践,您可以构建出高质量、可维护的测试套件,确保 Trae 应用程序的稳定性和可靠性。