08ea5fbe98
添加用户管理视图、API和状态管理文件
20 KiB
20 KiB
统一测试框架架构设计
设计目标
- 统一测试框架:整合多个测试框架,提供统一的配置、执行和报告机制
- 提升代码复用性:消除重复代码,提取公共测试工具类和配置
- 优化测试流程:简化测试执行,提高测试效率和稳定性
- 聚焦单元/集成测试:重点关注单元测试和集成测试
- 混合技术栈:Playwright用于E2E,Python 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天)
- 删除根目录过时测试脚本
- 删除根目录过时测试报告
- 删除 .trae/docs/ 过时文档
- 删除 docs/plans/ 过时测试计划
- 删除子项目过时文档
阶段2:统一测试框架(3-5天)
- 创建统一的核心模块
- 合并重复的helper类
- 创建统一的配置管理
- 创建统一的数据管理器
阶段3:优化测试配置(2-3天)
- 合并Playwright配置
- 统一环境变量配置
- 优化测试超时设置
- 配置并行执行
阶段4:生成新文档(2-3天)
- 生成使用指南
- 生成API文档
- 生成架构文档
- 生成最佳实践文档
阶段5:验证和优化(2-3天)
- 运行所有测试
- 验证测试覆盖率
- 优化测试性能
- 更新CI/CD配置
预期收益
- 代码复用性提升60%+:消除重复代码,统一测试工具类
- 测试效率提升40%+:统一测试执行入口,优化测试流程
- 维护成本降低50%+:统一配置管理,减少维护工作量
- 文档质量提升:删除过时文档,生成最新文档
风险评估
-
高风险:删除过时文件可能影响历史追溯
- 缓解措施:使用Git分支,保留备份
-
中风险:合并配置可能破坏现有测试
- 缓解措施:分阶段实施,充分测试
-
低风险:统一框架可能需要大量代码重构
- 缓解措施:逐步重构,保持向后兼容
设计时间: 2026-03-06 设计人员: 张翔(资深金融级高级自动化测试工程师) 版本: v1.0