577 lines
17 KiB
Markdown
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
|