Files
2026-01-26 08:04:53 +08:00

577 lines
17 KiB
Markdown

# 测试用例示例
## 单元测试示例
### utils/format.test.js
```javascript
import { describe, it, expect } from 'vitest';
import { formatMoney, formatPercentage, formatDate } from '../src/utils/format';
describe('formatMoney', () => {
it('TC-001-01: 应该正确格式化整数', () => {
expect(formatMoney(1234)).toBe('1,234.00');
});
it('TC-001-02: 应该正确格式化小数', () => {
expect(formatMoney(1234.56)).toBe('1,234.56');
});
it('TC-001-03: 应该处理千分位', () => {
expect(formatMoney(10000)).toBe('10,000.00');
});
it('TC-001-04: 应该处理零', () => {
expect(formatMoney(0)).toBe('0.00');
});
it('TC-001-05: 应该处理负数', () => {
expect(formatMoney(-1234.56)).toBe('-1,234.56');
});
it('TC-001-06: 应该四舍五入', () => {
expect(formatMoney(100.123)).toBe('100.12');
});
});
describe('formatPercentage', () => {
it('TC-002-01: 应该正确格式化50%', () => {
expect(formatPercentage(0.5)).toBe('50.00%');
});
it('TC-002-02: 应该正确格式化100%', () => {
expect(formatPercentage(1)).toBe('100.00%');
});
it('TC-002-03: 应该处理零', () => {
expect(formatPercentage(0)).toBe('0.00%');
});
it('TC-002-04: 应该保留两位小数', () => {
expect(formatPercentage(0.6666)).toBe('66.66%');
});
});
describe('formatDate', () => {
it('TC-003-01: 应该格式化日期字符串', () => {
expect(formatDate('2026-01-25')).toBe('2026-01-25');
});
it('TC-003-02: 应该格式化Date对象', () => {
expect(formatDate(new Date('2026-01-25'))).toBe('2026-01-25');
});
});
```
### components/Common/Button.test.jsx
```jsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
describe('Button Component - TC-009', () => {
it('TC-009-01: 应该渲染按钮文字', () => {
render(<Button>点击我</Button>);
expect(screen.getByText('点击我')).toBeInTheDocument();
});
it('TC-009-02: 应该正确显示primary类型', () => {
render(<Button type="primary">保存</Button>);
const button = screen.getByRole('button');
expect(button).toHaveClass('btn-primary');
});
it('TC-009-03: 应该正确显示default类型', () => {
render(<Button type="default">取消</Button>);
const button = screen.getByRole('button');
expect(button).toHaveClass('btn-default');
});
it('TC-009-04: 应该正确显示danger类型', () => {
render(<Button type="danger">删除</Button>);
const button = screen.getByRole('button');
expect(button).toHaveClass('btn-danger');
});
it('TC-009-05: 应该显示loading状态', () => {
render(<Button loading>加载中</Button>);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(button).toHaveClass('btn-loading');
});
it('TC-009-06: 应该禁用按钮', () => {
render(<Button disabled>禁用</Button>);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
});
it('TC-009-07: 应该调用onClick', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>点击</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
```
### hooks/useAuth.test.js
```javascript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { AuthProvider, useAuth } from '../src/contexts/AuthContext';
// 模拟authAPI
vi.mock('../src/services/auth', () => ({
authAPI: {
login: vi.fn(),
getCurrentUser: vi.fn(),
logout: vi.fn(),
},
}));
describe('useAuth Hook - TC-006', () => {
beforeEach(() => {
localStorage.clear();
vi.clearAllMocks();
});
it('TC-006-01: 初始状态应该是未认证', () => {
const { result } = renderHook(() => useAuth(), {
wrapper: AuthProvider,
});
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.user).toBe(null);
expect(result.current.token).toBe(null);
expect(result.current.loading).toBe(false);
});
it('TC-006-02: 应该正确处理登录', async () => {
const mockResponse = {
user: { id: 1, username: 'admin', role: 'admin' },
token: 'mock-token',
};
const { authAPI } = await import('../src/services/auth');
authAPI.login.mockResolvedValue(mockResponse);
const { result } = renderHook(() => useAuth(), {
wrapper: AuthProvider,
});
await act(async () => {
await result.current.login('admin', 'password123');
});
await waitFor(() => {
expect(result.current.isAuthenticated).toBe(true);
expect(result.current.user).toEqual(mockResponse.user);
expect(result.current.token).toBe(mockResponse.token);
expect(localStorage.getItem('token')).toBe(mockResponse.token);
expect(localStorage.getItem('user')).toBe(JSON.stringify(mockResponse.user));
});
});
it('TC-006-03: 应该正确处理登录失败', async () => {
const { authAPI } = await import('../src/services/auth');
authAPI.login.mockRejectedValue(new Error('用户名或密码错误'));
const { result } = renderHook(() => useAuth(), {
wrapper: AuthProvider,
});
try {
await act(async () => {
await result.current.login('admin', 'wrong');
});
} catch (error) {
expect(error.message).toBe('用户名或密码错误');
}
});
it('TC-006-04: 应该正确处理登出', async () => {
const { result } = renderHook(() => useAuth(), {
wrapper: AuthProvider,
});
// 先登录
const mockResponse = {
user: { id: 1, username: 'admin' },
token: 'mock-token',
};
const { authAPI } = await import('../src/services/auth');
authAPI.login.mockResolvedValue(mockResponse);
authAPI.logout.mockResolvedValue({});
await act(async () => {
await result.current.login('admin', 'password');
});
// 然后登出
await act(async () => {
await result.current.logout();
});
await waitFor(() => {
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.user).toBe(null);
expect(result.current.token).toBe(null);
expect(localStorage.getItem('token')).toBe(null);
expect(localStorage.getItem('user')).toBe(null);
});
});
it('TC-006-05: hasRole应该正确检查角色', async () => {
const { result } = renderHook(() => useAuth(), {
wrapper: AuthProvider,
});
const mockResponse = {
user: { id: 1, username: 'admin', role: 'admin' },
token: 'mock-token',
};
const { authAPI } = await import('../src/services/auth');
authAPI.login.mockResolvedValue(mockResponse);
await act(async () => {
await result.current.login('admin', 'password');
});
await waitFor(() => {
expect(result.current.hasRole(['admin'])).toBe(true);
expect(result.current.hasRole(['market'])).toBe(false);
expect(result.current.hasRole(['admin', 'market'])).toBe(true);
});
});
});
```
## 集成测试示例
### pages/Login/Login.test.jsx
```jsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import Login from './Login';
// 模拟authAPI
vi.mock('../../services/auth', () => ({
authAPI: {
login: vi.fn(),
},
}));
// 模拟useNavigate
const mockedNavigate = vi.fn();
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return {
...actual,
useNavigate: () => mockedNavigate,
};
});
describe('Login Page - TC-015', () => {
const renderLogin = () => {
return render(
<BrowserRouter>
<Login />
</BrowserRouter>
);
};
it('TC-015-01: 应该渲染登录表单', () => {
renderLogin();
expect(screen.getByText('海洋项目管理系统')).toBeInTheDocument();
expect(screen.getByLabelText(/用户名/)).toBeInTheDocument();
expect(screen.getByLabelText(/密码/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /登录/ })).toBeInTheDocument();
});
it('TC-015-02: 应该显示必填字段验证错误', async () => {
renderLogin();
const submitButton = screen.getByRole('button', { name: /登录/ });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/请输入用户名/)).toBeInTheDocument();
expect(screen.getByText(/请输入密码/)).toBeInTheDocument();
});
});
it('TC-015-03: 应该显示用户名长度错误', async () => {
renderLogin();
const usernameInput = screen.getByLabelText(/用户名/);
fireEvent.change(usernameInput, { target: { value: 'ab' } });
fireEvent.blur(usernameInput);
await waitFor(() => {
expect(screen.getByText(/用户名至少3个字符/)).toBeInTheDocument();
});
});
it('TC-015-04: 应该显示密码长度错误', async () => {
renderLogin();
const passwordInput = screen.getByLabelText(/密码/);
fireEvent.change(passwordInput, { target: { value: '12345' } });
fireEvent.blur(passwordInput);
await waitFor(() => {
expect(screen.getByText(/密码至少6个字符/)).toBeInTheDocument();
});
});
it('TC-015-05: 应该成功登录', async () => {
const { authAPI } = await import('../../services/auth');
const mockResponse = {
user: { id: 1, username: 'admin', real_name: '系统管理员', role: 'admin' },
token: 'mock-token',
};
authAPI.login.mockResolvedValue(mockResponse);
renderLogin();
const usernameInput = screen.getByLabelText(/用户名/);
const passwordInput = screen.getByLabelText(/密码/);
const submitButton = screen.getByRole('button', { name: /登录/ });
fireEvent.change(usernameInput, { target: { value: 'admin' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(authAPI.login).toHaveBeenCalledWith('admin', 'password123');
expect(mockedNavigate).toHaveBeenCalledWith('/dashboard');
});
});
it('TC-015-06: 应该显示登录失败错误', async () => {
const { authAPI } = await import('../../services/auth');
authAPI.login.mockRejectedValue(new Error('用户名或密码错误'));
renderLogin();
const usernameInput = screen.getByLabelText(/用户名/);
const passwordInput = screen.getByLabelText(/密码/);
const submitButton = screen.getByRole('button', { name: /登录/ });
fireEvent.change(usernameInput, { target: { value: 'admin' } });
fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/用户名或密码错误/)).toBeInTheDocument();
});
});
it('TC-015-07: 应该处理Enter键提交', async () => {
const { authAPI } = await import('../../services/auth');
const mockResponse = {
user: { id: 1, username: 'admin' },
token: 'mock-token',
};
authAPI.login.mockResolvedValue(mockResponse);
renderLogin();
const usernameInput = screen.getByLabelText(/用户名/);
const passwordInput = screen.getByLabelText(/密码/);
fireEvent.change(usernameInput, { target: { value: 'admin' } });
fireEvent.change(passwordInput, { target: { value: 'password123' } });
fireEvent.keyDown(passwordInput, { key: 'Enter' });
await waitFor(() => {
expect(authAPI.login).toHaveBeenCalledWith('admin', 'password123');
});
});
});
```
## E2E测试示例
### login.spec.js
```javascript
import { test, expect } from '@playwright/test';
test.describe('登录流程 - TC-E2E-001', () => {
test.beforeEach(async ({ page }) => {
// 清除localStorage
await page.goto('http://localhost:3000');
await page.evaluate(() => localStorage.clear());
});
test('TC-E2E-001-01: 应该成功登录并跳转到仪表盘', async ({ page }) => {
// 访问登录页
await page.goto('http://localhost:3000/login');
// 输入用户名和密码
await page.fill('input[name="username"]', 'admin');
await page.fill('input[name="password"]', 'password123');
// 点击登录按钮
await page.click('button[type="submit"]');
// 验证跳转到仪表盘
await expect(page).toHaveURL('http://localhost:3000/dashboard');
// 验证用户信息显示
await expect(page.locator('.user-info')).toContainText('系统管理员');
});
test('TC-E2E-001-02: 应该显示登录失败错误', async ({ page }) => {
await page.goto('http://localhost:3000/login');
// 输入错误密码
await page.fill('input[name="username"]', 'admin');
await page.fill('input[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
// 验证错误提示
await expect(page.locator('.error-message')).toContainText('用户名或密码错误');
});
test('TC-E2E-001-03: 应该验证必填字段', async ({ page }) => {
await page.goto('http://localhost:3000/login');
// 直接点击登录按钮
await page.click('button[type="submit"]');
// 验证错误提示
await expect(page.locator('text=请输入用户名')).toBeVisible();
await expect(page.locator('text=请输入密码')).toBeVisible();
});
test('TC-E2E-001-04: 应该记住密码', async ({ page }) => {
await page.goto('http://localhost:3000/login');
// 勾选记住密码
await page.check('input[type="checkbox"]');
// 输入用户名和密码
await page.fill('input[name="username"]', 'admin');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
// 等待登录成功
await page.waitForURL('**/dashboard');
// 刷新页面
await page.reload();
// 验证自动填充
await expect(page.locator('input[name="username"]')).toHaveValue('admin');
await expect(page.locator('input[name="password"]')).toHaveValue('password123');
});
});
```
### projects.spec.js
```javascript
import { test, expect } from '@playwright/test';
test.describe('项目管理 - TC-E2E-003 to TC-E2E-006', () => {
test.beforeEach(async ({ page }) => {
// 登录
await page.goto('http://localhost:3000/login');
await page.fill('input[name="username"]', 'admin');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
});
test('TC-E2E-003: 应该显示项目列表', async ({ page }) => {
// 导航到项目列表
await page.click('text=项目管理');
await page.waitForURL('**/projects');
// 验证表格显示
await expect(page.locator('table')).toBeVisible();
await expect(page.locator('th:has-text("合同编号")')).toBeVisible();
await expect(page.locator('th:has-text("项目名称")')).toBeVisible();
});
test('TC-E2E-004: 应该创建新项目', async ({ page }) => {
await page.click('text=项目管理');
await page.waitForURL('**/projects');
// 点击新建项目按钮
await page.click('button:has-text("新建项目")');
// 填写表单
await page.fill('input[name="project_no"]', 'PRJ999');
await page.fill('input[name="name"]', 'E2E测试项目');
await page.selectOption('select[name="engineering_type"]', '基建');
await page.fill('input[name="contract_amount"]', '100');
await page.click('input[name="signing_date"]');
await page.fill('input[name="signing_date"]', '2026-01-25');
// 提交表单
await page.click('button:has-text("保存")');
// 验证成功提示
await expect(page.locator('.success-message')).toContainText('项目创建成功');
// 验证项目出现在列表中
await expect(page.locator('table')).toContainText('PRJ999');
});
test('TC-E2E-005: 应该编辑项目', async ({ page }) => {
await page.click('text=项目管理');
await page.waitForURL('**/projects');
// 点击编辑按钮
await page.click('button:has-text("编辑")');
// 修改项目名称
await page.fill('input[name="name"]', 'E2E更新项目');
await page.fill('input[name="contract_amount"]', '200');
// 提交表单
await page.click('button:has-text("保存")');
// 验证成功提示
await expect(page.locator('.success-message')).toContainText('项目更新成功');
// 验证列表显示更新后的数据
await expect(page.locator('table')).toContainText('E2E更新项目');
});
test('TC-E2E-006: 应该删除项目', async ({ page }) => {
await page.click('text=项目管理');
await page.waitForURL('**/projects');
// 点击删除按钮
await page.click('button:has-text("删除")');
// 确认删除
await page.click('button:has-text("确定")');
// 验证成功提示
await expect(page.locator('.success-message')).toContainText('项目删除成功');
});
});
```
---
**文档维护**: 前端程序员
**文档类型**: 测试用例示例
**最后更新**: 2026-01-25