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

17 KiB

测试用例示例

单元测试示例

utils/format.test.js

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

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

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

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

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

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