# 测试用例示例 ## 单元测试示例 ### 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(); expect(screen.getByText('点击我')).toBeInTheDocument(); }); it('TC-009-02: 应该正确显示primary类型', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('btn-primary'); }); it('TC-009-03: 应该正确显示default类型', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('btn-default'); }); it('TC-009-04: 应该正确显示danger类型', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('btn-danger'); }); it('TC-009-05: 应该显示loading状态', () => { render(); const button = screen.getByRole('button'); expect(button).toBeDisabled(); expect(button).toHaveClass('btn-loading'); }); it('TC-009-06: 应该禁用按钮', () => { render(); const button = screen.getByRole('button'); expect(button).toBeDisabled(); }); it('TC-009-07: 应该调用onClick', () => { const handleClick = vi.fn(); render(); 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( ); }; 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