Files
everything-is-suitable/docs/plans/2026-03-06-unified-test-framework-architecture.md
T
张翔 08ea5fbe98 feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
2026-03-28 14:37:29 +08:00

20 KiB
Raw Blame History

统一测试框架架构设计

设计目标

  1. 统一测试框架:整合多个测试框架,提供统一的配置、执行和报告机制
  2. 提升代码复用性:消除重复代码,提取公共测试工具类和配置
  3. 优化测试流程:简化测试执行,提高测试效率和稳定性
  4. 聚焦单元/集成测试:重点关注单元测试和集成测试
  5. 混合技术栈Playwright用于E2EPython pytest用于API测试

架构设计

1. 整体架构

everything-is-suitable-test/          # 统一测试框架根目录
│
├── e2e/                              # E2E测试层(Playwright + TypeScript
│   ├── core/                         # 核心模块
│   │   ├── test-config.ts           # 统一配置管理
│   │   ├── test-logger.ts           # 统一日志记录
│   │   ├── test-reporter.ts         # 统一报告生成
│   │   └── test-data-manager.ts    # 统一数据管理
│   ├── helpers/                     # 测试辅助工具
│   │   ├── form-helper.ts          # 表单操作辅助
│   │   ├── table-helper.ts         # 表格操作辅助
│   │   ├── screenshot-helper.ts    # 截图辅助
│   │   ├── api-helper.ts           # API请求辅助
│   │   └── assertion-helper.ts     # 断言辅助
│   ├── pages/                       # 页面对象模型(POM)
│   │   ├── base-page.ts            # 基础页面类
│   │   ├── login-page.ts           # 登录页面
│   │   ├── dashboard-page.ts       # 仪表盘页面
│   │   ├── user-management-page.ts # 用户管理页面
│   │   └── role-management-page.ts # 角色管理页面
│   ├── fixtures/                    # 测试夹具
│   │   └── test-fixtures.ts        # 自定义测试夹具
│   └── tests/                       # E2E测试用例
│       ├── admin/                  # 管理系统E2E测试
│       │   ├── auth.spec.ts
│       │   ├── user-management.spec.ts
│       │   └── role-management.spec.ts
│       ├── uniapp/                 # UniApp E2E测试
│       │   ├── calendar.spec.ts
│       │   └── almanac.spec.ts
│       └── integration/             # 集成测试
│           └── cross-module.spec.ts
│
├── api/                             # API测试层(Python pytest
│   ├── core/                        # 核心模块
│   │   ├── config_manager.py      # 配置管理
│   │   ├── logger_manager.py       # 日志管理
│   │   ├── test_engine.py          # 测试引擎
│   │   └── validation_engine.py    # 验证引擎
│   ├── helpers/                     # 辅助工具
│   │   ├── api_client.py           # API客户端
│   │   ├── auth_manager.py         # 认证管理
│   │   └── data_factory.py         # 数据工厂
│   ├── models/                      # 数据模型
│   │   ├── test_models.py          # 测试模型
│   │   └── exceptions.py           # 异常定义
│   ├── orchestrator/                # 测试编排
│   │   └── test_orchestrator.py    # 测试编排器
│   ├── report/                      # 报告生成
│   │   └── report_manager.py      # 报告管理器
│   └── tests/                       # API测试用例
│       ├── unit/                   # 单元测试
│       │   ├── test_config_manager.py
│       │   ├── test_api_client.py
│       │   └── test_data_factory.py
│       ├── integration/            # 集成测试
│       │   ├── test_user_api.py
│       │   ├── test_role_api.py
│       │   └── test_menu_api.py
│       └── e2e/                    # E2E API测试
│           └── test_complete_flow.py
│
├── unit/                            # 单元测试层
│   ├── admin/                       # 前端单元测试(Vitest
│   │   ├── services/
│   │   │   ├── auth.service.test.ts
│   │   │   ├── user.service.test.ts
│   │   │   └── role.service.test.ts
│   │   ├── stores/
│   │   │   ├── auth.store.test.ts
│   │   │   └── user.store.test.ts
│   │   └── components/
│   │       └── *.test.ts
│   ├── uniapp/                      # UniApp单元测试(Vitest
│   │   └── services/
│   │       ├── calendarService.test.ts
│   │       └── cacheService.test.ts
│   └── backend/                     # 后端单元测试(JUnit)
│       └── [保留在各模块的src/test/java/目录]
│
├── config/                          # 统一配置
│   ├── playwright.config.ts         # Playwright配置
│   ├── vitest.config.ts            # Vitest配置
│   ├── pytest.ini                   # pytest配置
│   └── test-config.yml             # 测试配置
│
├── scripts/                         # 测试脚本
│   ├── run-all-tests.sh            # 运行所有测试
│   ├── run-e2e-tests.sh           # 运行E2E测试
│   ├── run-api-tests.sh           # 运行API测试
│   ├── run-unit-tests.sh          # 运行单元测试
│   ├── cleanup.sh                 # 清理测试环境
│   ├── generate-report.sh         # 生成测试报告
│   └── setup-test-env.sh         # 设置测试环境
│
├── docs/                            # 测试文档
│   ├── README.md                  # 使用指南
│   ├── ARCHITECTURE.md            # 架构设计
│   ├── API.md                     # API文档
│   └── BEST_PRACTICES.md         # 最佳实践
│
├── package.json                    # Node.js依赖
├── pyproject.toml                  # Python依赖
├── tsconfig.json                   # TypeScript配置
└── .env.example                    # 环境变量示例

2. 核心模块设计

2.1 配置管理(test-config.ts

export interface TestEnvironment {
  name: string;
  baseURL: string;
  apiBaseURL: string;
  uniappBaseURL: string;
  mockEnabled: boolean;
  timeout: number;
  credentials: {
    username: string;
    password: string;
  };
}

export class TestConfig {
  private static instance: TestConfig;
  private currentEnv: TestEnvironment;

  private constructor() {
    this.currentEnv = this.loadEnvironment();
  }

  static getInstance(): TestConfig {
    if (!TestConfig.instance) {
      TestConfig.instance = new TestConfig();
    }
    return TestConfig.instance;
  }

  getEnvironment(): TestEnvironment {
    return this.currentEnv;
  }

  setEnvironment(envName: string): void {
    this.currentEnv = this.loadEnvironment(envName);
  }

  private loadEnvironment(envName?: string): TestEnvironment {
    const name = envName || process.env.TEST_ENV || 'local';
    
    return {
      name,
      baseURL: process.env.ADMIN_BASE_URL || 'http://localhost:5174',
      apiBaseURL: process.env.API_BASE_URL || 'http://127.0.0.1:8080',
      uniappBaseURL: process.env.UNIAPP_BASE_URL || 'http://localhost:8081',
      mockEnabled: process.env.MOCK_ENABLED === 'true',
      timeout: parseInt(process.env.TEST_TIMEOUT || '30000'),
      credentials: {
        username: process.env.TEST_USERNAME || 'admin',
        password: process.env.TEST_PASSWORD || 'admin123'
      }
    };
  }
}

export const testConfig = TestConfig.getInstance();

2.2 日志管理(test-logger.ts

export enum LogLevel {
  DEBUG = 'DEBUG',
  INFO = 'INFO',
  WARN = 'WARN',
  ERROR = 'ERROR'
}

export class TestLogger {
  private static instance: TestLogger;
  private logs: Array<{ level: LogLevel; message: string; timestamp: Date }> = [];

  private constructor() {}

  static getInstance(): TestLogger {
    if (!TestLogger.instance) {
      TestLogger.instance = new TestLogger();
    }
    return TestLogger.instance;
  }

  debug(message: string): void {
    this.log(LogLevel.DEBUG, message);
  }

  info(message: string): void {
    this.log(LogLevel.INFO, message);
  }

  warn(message: string): void {
    this.log(LogLevel.WARN, message);
  }

  error(message: string, error?: Error): void {
    this.log(LogLevel.ERROR, message);
    if (error) {
      console.error(error);
    }
  }

  startTest(testName: string): void {
    this.info(`\n========== 开始测试: ${testName} ==========`);
  }

  endTest(testName: string, status: string): void {
    this.info(`========== 结束测试: ${testName} (${status}) ==========`);
  }

  startStep(stepName: string): void {
    this.info(`  [步骤] ${stepName}`);
  }

  endStep(stepName: string, status: string): void {
    this.info(`  [步骤] ${stepName} (${status})`);
  }

  private log(level: LogLevel, message: string): void {
    const timestamp = new Date();
    this.logs.push({ level, message, timestamp });
    
    const logMessage = `[${timestamp.toISOString()}] [${level}] ${message}`;
    console.log(logMessage);
  }

  getLogs(): Array<{ level: LogLevel; message: string; timestamp: Date }> {
    return this.logs;
  }

  clearLogs(): void {
    this.logs = [];
  }
}

export const testLogger = TestLogger.getInstance();

2.3 数据管理(test-data-manager.ts

export interface TestUser {
  id?: number;
  username: string;
  password: string;
  realName: string;
  email: string;
  phone?: string;
}

export interface TestRole {
  id?: number;
  roleName: string;
  roleCode: string;
  description?: string;
}

export class TestDataManager {
  private static instance: TestDataManager;
  private testData: Map<string, any> = new Map();

  private constructor() {}

  static getInstance(): TestDataManager {
    if (!TestDataManager.instance) {
      TestDataManager.instance = new TestDataManager();
    }
    return TestDataManager.instance;
  }

  async createTestUser(userData: Partial<TestUser>): Promise<TestUser> {
    const user: TestUser = {
      username: `test_${Date.now()}`,
      password: 'Test123456',
      realName: '测试用户',
      email: `test_${Date.now()}@example.com`,
      ...userData
    };

    this.testData.set(`user_${user.username}`, user);
    return user;
  }

  async createTestRole(roleData: Partial<TestRole>): Promise<TestRole> {
    const role: TestRole = {
      roleName: `测试角色_${Date.now()}`,
      roleCode: `TEST_ROLE_${Date.now()}`,
      ...roleData
    };

    this.testData.set(`role_${role.roleCode}`, role);
    return role;
  }

  getTestData(key: string): any {
    return this.testData.get(key);
  }

  async cleanup(): Promise<void> {
    this.testData.clear();
  }
}

export const testDataManager = TestDataManager.getInstance();

3. 测试辅助工具设计

3.1 表单辅助(form-helper.ts

import { Page, Locator } from '@playwright/test';

export class FormHelper {
  constructor(private page: Page) {}

  async fillField(selector: string, value: string): Promise<void> {
    await this.page.fill(selector, value);
  }

  async fillForm(fields: Record<string, { value: string; timeout?: number }>): Promise<void> {
    for (const [selector, config] of Object.entries(fields)) {
      await this.page.fill(selector, config.value);
    }
  }

  async selectOption(selector: string, value: string): Promise<void> {
    await this.page.selectOption(selector, value);
  }

  async checkCheckbox(selector: string): Promise<void> {
    await this.page.check(selector);
  }

  async uncheckCheckbox(selector: string): Promise<void> {
    await this.page.uncheck(selector);
  }

  async submitForm(selector?: string): Promise<void> {
    if (selector) {
      await this.page.click(selector);
    } else {
      await this.page.keyboard.press('Enter');
    }
  }

  async clearField(selector: string): Promise<void> {
    await this.page.fill(selector, '');
  }
}

3.2 表格辅助(table-helper.ts

import { Page } from '@playwright/test';

export class TableHelper {
  constructor(private page: Page) {}

  async getRowCount(tableSelector: string): Promise<number> {
    const rows = await this.page.locator(`${tableSelector} tbody tr`).count();
    return rows;
  }

  async getCellText(tableSelector: string, row: number, col: number): Promise<string> {
    const cell = await this.page.locator(
      `${tableSelector} tbody tr:nth-child(${row}) td:nth-child(${col})`
    );
    return await cell.textContent() || '';
  }

  async findRowsByCellText(tableSelector: string, searchText: string): Promise<number[]> {
    const rows: number[] = [];
    const rowCount = await this.getRowCount(tableSelector);
    
    for (let i = 1; i <= rowCount; i++) {
      const rowText = await this.page.locator(
        `${tableSelector} tbody tr:nth-child(${i})`
      ).textContent();
      
      if (rowText?.includes(searchText)) {
        rows.push(i);
      }
    }
    
    return rows;
  }

  async clickRow(tableSelector: string, row: number): Promise<void> {
    await this.page.click(`${tableSelector} tbody tr:nth-child(${row})`);
  }

  async clickCell(tableSelector: string, row: number, col: number): Promise<void> {
    await this.page.click(
      `${tableSelector} tbody tr:nth-child(${row}) td:nth-child(${col})`
    );
  }
}

4. 统一测试执行流程

4.1 package.json 脚本

{
  "scripts": {
    "test": "npm run test:all",
    "test:all": "npm run test:unit && npm run test:api && npm run test:e2e",
    "test:unit": "npm run test:unit:admin && npm run test:unit:uniapp",
    "test:unit:admin": "cd ../everything-is-suitable-admin && npm run test",
    "test:unit:uniapp": "cd ../everything-is-suitable-uniapp && npm run test",
    "test:api": "cd api && pytest tests/ -v",
    "test:e2e": "playwright test",
    "test:e2e:admin": "playwright test e2e/tests/admin/",
    "test:e2e:uniapp": "playwright test e2e/tests/uniapp/",
    "test:e2e:headed": "playwright test --headed",
    "test:e2e:debug": "playwright test --debug",
    "test:e2e:ui": "playwright test --ui",
    "test:report": "playwright show-report",
    "test:cleanup": "./scripts/cleanup.sh",
    "test:setup": "./scripts/setup-test-env.sh"
  }
}

4.2 统一配置文件(playwright.config.ts

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e/tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 2 : 4,
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results/results.json' }],
    ['junit', { outputFile: 'test-results/junit.xml' }]
  ],
  use: {
    baseURL: process.env.ADMIN_BASE_URL || 'http://localhost:5174',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    actionTimeout: 30000,
    navigationTimeout: 30000
  },
  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 dev',
    url: 'http://localhost:5174',
    reuseExistingServer: !process.env.CI,
    timeout: 120000,
  },
});

5. 测试报告统一

5.1 报告生成器(test-reporter.ts

export interface TestResult {
  testName: string;
  status: 'passed' | 'failed' | 'skipped';
  duration: number;
  error?: string;
  screenshot?: string;
}

export class TestReporter {
  private results: TestResult[] = [];

  addResult(result: TestResult): void {
    this.results.push(result);
  }

  generateJSON(): string {
    return JSON.stringify({
      timestamp: new Date().toISOString(),
      total: this.results.length,
      passed: this.results.filter(r => r.status === 'passed').length,
      failed: this.results.filter(r => r.status === 'failed').length,
      skipped: this.results.filter(r => r.status === 'skipped').length,
      results: this.results
    }, null, 2);
  }

  generateHTML(): string {
    const passed = this.results.filter(r => r.status === 'passed').length;
    const failed = this.results.filter(r => r.status === 'failed').length;
    const skipped = this.results.filter(r => r.status === 'skipped').length;

    return `
<!DOCTYPE html>
<html>
<head>
  <title>测试报告</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 20px; }
    .summary { background: #f5f5f5; padding: 20px; margin-bottom: 20px; }
    .passed { color: green; }
    .failed { color: red; }
    .skipped { color: orange; }
    table { border-collapse: collapse; width: 100%; }
    th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
    th { background-color: #4CAF50; color: white; }
  </style>
</head>
<body>
  <h1>测试报告</h1>
  <div class="summary">
    <p>总测试数: ${this.results.length}</p>
    <p class="passed">通过: ${passed}</p>
    <p class="failed">失败: ${failed}</p>
    <p class="skipped">跳过: ${skipped}</p>
  </div>
  <table>
    <tr>
      <th>测试名称</th>
      <th>状态</th>
      <th>耗时</th>
      <th>错误</th>
    </tr>
    ${this.results.map(r => `
      <tr>
        <td>${r.testName}</td>
        <td class="${r.status}">${r.status}</td>
        <td>${r.duration}ms</td>
        <td>${r.error || ''}</td>
      </tr>
    `).join('')}
  </table>
</body>
</html>
    `;
  }
}

6. 环境变量统一

6.1 .env.example

# 测试环境配置
TEST_ENV=local

# 服务地址
ADMIN_BASE_URL=http://localhost:5174
UNIAPP_BASE_URL=http://localhost:8081
API_BASE_URL=http://127.0.0.1:8080

# 测试账号
TEST_USERNAME=admin
TEST_PASSWORD=admin123

# Mock配置
MOCK_ENABLED=false

# 超时配置
TEST_TIMEOUT=30000

# 数据库配置
DB_HOST=localhost
DB_PORT=3306
DB_NAME=test_db
DB_USERNAME=root
DB_PASSWORD=root

# 报告配置
REPORT_DIR=test-results
REPORT_FORMAT=json,html,junit

实施计划

阶段1:清理过时文件(1-2天)

  1. 删除根目录过时测试脚本
  2. 删除根目录过时测试报告
  3. 删除 .trae/docs/ 过时文档
  4. 删除 docs/plans/ 过时测试计划
  5. 删除子项目过时文档

阶段2:统一测试框架(3-5天)

  1. 创建统一的核心模块
  2. 合并重复的helper类
  3. 创建统一的配置管理
  4. 创建统一的数据管理器

阶段3:优化测试配置(2-3天)

  1. 合并Playwright配置
  2. 统一环境变量配置
  3. 优化测试超时设置
  4. 配置并行执行

阶段4:生成新文档(2-3天)

  1. 生成使用指南
  2. 生成API文档
  3. 生成架构文档
  4. 生成最佳实践文档

阶段5:验证和优化(2-3天)

  1. 运行所有测试
  2. 验证测试覆盖率
  3. 优化测试性能
  4. 更新CI/CD配置

预期收益

  1. 代码复用性提升60%+:消除重复代码,统一测试工具类
  2. 测试效率提升40%+:统一测试执行入口,优化测试流程
  3. 维护成本降低50%+:统一配置管理,减少维护工作量
  4. 文档质量提升:删除过时文档,生成最新文档

风险评估

  1. 高风险:删除过时文件可能影响历史追溯

    • 缓解措施:使用Git分支,保留备份
  2. 中风险:合并配置可能破坏现有测试

    • 缓解措施:分阶段实施,充分测试
  3. 低风险:统一框架可能需要大量代码重构

    • 缓解措施:逐步重构,保持向后兼容

设计时间: 2026-03-06 设计人员: 张翔(资深金融级高级自动化测试工程师) 版本: v1.0