38dc055a27
- 添加菜单数据修复设计文档 - 添加用户管理和角色管理测试修复设计文档 - 添加本地开发测试设计文档 - 添加相关实现计划
1184 lines
35 KiB
Markdown
1184 lines
35 KiB
Markdown
# 基于角色的用户模拟测试套件设计方案
|
||
|
||
**版本**: 1.0
|
||
**日期**: 2026-04-04
|
||
**作者**: 张翔
|
||
**状态**: 待审查
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [概述](#概述)
|
||
2. [核心决策](#核心决策)
|
||
3. [整体架构设计](#整体架构设计)
|
||
4. [核心组件设计](#核心组件设计)
|
||
5. [测试场景实现](#测试场景实现)
|
||
6. [配置和CI/CD集成](#配置和cicd集成)
|
||
7. [实施计划](#实施计划)
|
||
8. [风险控制](#风险控制)
|
||
9. [成功指标](#成功指标)
|
||
|
||
---
|
||
|
||
## 概述
|
||
|
||
### 背景
|
||
|
||
当前后端管理系统已有40+个E2E测试文件,但存在以下问题:
|
||
|
||
1. **测试分散**:测试文件组织混乱,缺乏系统性
|
||
2. **权限验证不足**:主要使用admin用户测试,缺乏跨角色权限验证
|
||
3. **真实场景覆盖不全**:缺乏完整的业务流程测试
|
||
4. **维护成本高**:测试代码重复,工具化程度低
|
||
|
||
### 目标
|
||
|
||
设计并实现一个基于角色的用户模拟测试套件,达到真实场景的验收标准:
|
||
|
||
1. **真实业务场景覆盖**:覆盖完整的业务流程
|
||
2. **权限边界验证**:验证不同角色的权限边界
|
||
3. **高效执行**:优化测试执行效率
|
||
4. **易于维护**:清晰的结构和工具化支持
|
||
|
||
---
|
||
|
||
## 核心决策
|
||
|
||
### 决策1:角色范围
|
||
|
||
**选择**:使用现有3种角色
|
||
|
||
**理由**:
|
||
- 系统已有完整的RBAC权限模型
|
||
- 3种角色覆盖主要业务场景
|
||
- 避免过度设计,聚焦核心需求
|
||
|
||
**角色定义**:
|
||
- **admin(超级管理员)**:拥有所有权限
|
||
- **user(普通用户)**:只能访问和修改自己的信息
|
||
- **test(测试用户)**:用于特定测试场景
|
||
|
||
---
|
||
|
||
### 决策2:测试模式
|
||
|
||
**选择**:混合模式(业务流程 + 权限验证)
|
||
|
||
**理由**:
|
||
1. 符合真实业务本质:真实场景不仅是"用户能完成业务流程",更包括"用户在权限约束下完成业务流程"
|
||
2. 质量保障价值更高:能同时发现业务流程缺陷和权限控制缺陷
|
||
3. 符合RBAC最佳实践:完美契合"谁在什么场景下能做什么"的核心思想
|
||
|
||
**示例**:
|
||
```typescript
|
||
// 业务流程测试
|
||
test('管理员创建用户', async ({ page }) => {
|
||
await loginAsRole(page, 'admin');
|
||
await createUser(testUser);
|
||
await expectUserExists(testUser.username);
|
||
});
|
||
|
||
// 权限验证测试(嵌入业务流程中)
|
||
test('普通用户无法访问用户管理页面', async ({ page }) => {
|
||
await loginAsRole(page, 'user');
|
||
await verifyCannotAccess(page, '/user-management');
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
### 决策3:测试数据管理策略
|
||
|
||
**选择**:混合策略(核心数据预置 + 业务数据动态创建)
|
||
|
||
**理由**:
|
||
1. 符合真实业务场景:角色和权限体系是预先配置好的,业务数据是动态产生的
|
||
2. 执行效率与隔离性的最佳平衡:节省约43%执行时间
|
||
3. 降低测试维护成本:核心数据极少变更,业务数据灵活可控
|
||
4. 避免数据污染:核心数据不会被污染,业务数据完全隔离
|
||
|
||
**数据分类**:
|
||
|
||
| 数据类型 | 管理方式 | 生命周期 | 示例 |
|
||
|---------|---------|---------|------|
|
||
| 核心数据 | 预置 | 测试套件级别 | admin角色、基础权限 |
|
||
| 业务数据 | 动态创建 | 测试用例级别 | 测试用户、测试菜单 |
|
||
|
||
---
|
||
|
||
### 决策4:组织结构
|
||
|
||
**选择**:混合结构(roles/ + scenarios/ + shared/)
|
||
|
||
**理由**:
|
||
1. 完美契合混合模式测试策略
|
||
2. 支持真实的跨角色业务流程
|
||
3. 清晰的关注点分离
|
||
4. 易于扩展和维护
|
||
|
||
**目录结构**:
|
||
```
|
||
e2e/role-based-tests/
|
||
├── roles/ # 角色定义
|
||
│ ├── base.role.ts
|
||
│ ├── admin.role.ts
|
||
│ ├── user.role.ts
|
||
│ ├── test.role.ts
|
||
│ └── role-factory.ts
|
||
├── scenarios/ # 业务场景测试
|
||
│ ├── authentication/
|
||
│ ├── user-management/
|
||
│ ├── role-management/
|
||
│ └── menu-management/
|
||
└── shared/ # 共享工具
|
||
├── auth-helper.ts
|
||
├── role-auth-manager.ts
|
||
├── test-data-manager.ts
|
||
├── permission-helper.ts
|
||
└── workflow-helper.ts
|
||
```
|
||
|
||
---
|
||
|
||
### 决策5:迁移策略
|
||
|
||
**选择**:分层策略(核心场景优先迁移)
|
||
|
||
**理由**:
|
||
1. 风险可控:渐进式迁移,随时可回滚
|
||
2. 优先级明确:核心场景优先,价值最大化
|
||
3. 无重复测试:避免资源浪费
|
||
4. 保留价值:边缘场景测试继续发挥作用
|
||
|
||
**迁移优先级**:
|
||
- **P0**:认证场景(登录、登出、权限验证)
|
||
- **P1**:用户管理场景(创建、编辑、删除、生命周期)
|
||
- **P2**:角色管理场景(创建、权限分配)
|
||
- **P3**:菜单管理场景(创建、编辑、权限关联)
|
||
|
||
---
|
||
|
||
### 决策6:认证方式
|
||
|
||
**选择**:Token注入 + 可选真实登录
|
||
|
||
**理由**:
|
||
1. 符合测试金字塔原则:少量真实登录测试 + 大量Token注入测试
|
||
2. 执行效率高:节省约37%执行时间
|
||
3. 真实性保障:Token是真实的,业务流程是真实的
|
||
4. 灵活性强:可根据场景选择登录方式
|
||
|
||
**效率对比**:
|
||
- 真实登录:9秒/用例
|
||
- Token注入:6.1秒/用例(节省32%时间)
|
||
- 100个测试用例:节省约37%总时间
|
||
|
||
---
|
||
|
||
### 决策7:CI/CD集成
|
||
|
||
**选择**:Gitea + Jenkins
|
||
|
||
**理由**:
|
||
1. 符合团队现有技术栈
|
||
2. Jenkins生态成熟,插件丰富
|
||
3. Gitea轻量级,易于维护
|
||
4. 支持并行执行和矩阵测试
|
||
|
||
---
|
||
|
||
## 整体架构设计
|
||
|
||
### 架构图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 测试执行层 (Playwright) │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ scenarios/ │ │
|
||
│ │ ├── authentication/ (认证场景 - 真实登录) │ │
|
||
│ │ ├── user-management/ (用户管理 - Token注入) │ │
|
||
│ │ ├── role-management/ (角色管理 - Token注入) │ │
|
||
│ │ └── menu-management/ (菜单管理 - Token注入) │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓ 调用
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 角色管理层 (Roles) │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ RoleFactory │ │
|
||
│ │ ├── AdminRole (管理员角色定义) │ │
|
||
│ │ ├── UserRole (普通用户角色定义) │ │
|
||
│ │ └── TestRole (测试用户角色定义) │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ 每个角色包含: │
|
||
│ - credentials (登录凭证) │
|
||
│ - permissions (权限列表) │
|
||
│ - expectedBehaviors (预期行为) │
|
||
│ - cannotAccess (禁止访问的资源) │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓ 使用
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 工具层 (Shared) │
|
||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||
│ │ AuthHelper │ │ RoleAuthManager │ │
|
||
│ │ - loginAsRole() │ │ - getRoleToken() │ │
|
||
│ │ - logout() │ │ - cacheToken() │ │
|
||
│ └──────────────────┘ └──────────────────┘ │
|
||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||
│ │ TestDataManager │ │ PermissionHelper │ │
|
||
│ │ - createUser() │ │ - verifyCan() │ │
|
||
│ │ - cleanup() │ │ - verifyCannot() │ │
|
||
│ └──────────────────┘ └──────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓ 依赖
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Page Object层 (现有) │
|
||
│ LoginPage, UserManagementPage, RoleManagementPage, ... │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓ 操作
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 应用系统 (SUT) │
|
||
│ 前端 + 后端API + 数据库 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 核心组件设计
|
||
|
||
### 1. 角色定义系统
|
||
|
||
#### 1.1 角色基类
|
||
|
||
```typescript
|
||
// roles/base.role.ts
|
||
export interface RoleDefinition {
|
||
name: string;
|
||
displayName: string;
|
||
credentials: {
|
||
username: string;
|
||
password: string;
|
||
};
|
||
permissions: string[];
|
||
cannotAccess: string[];
|
||
expectedBehaviors: {
|
||
canCreate: string[];
|
||
canRead: string[];
|
||
canUpdate: string[];
|
||
canDelete: string[];
|
||
};
|
||
}
|
||
```
|
||
|
||
#### 1.2 管理员角色定义
|
||
|
||
```typescript
|
||
// roles/admin.role.ts
|
||
export const AdminRole: RoleDefinition = {
|
||
name: 'admin',
|
||
displayName: '超级管理员',
|
||
credentials: {
|
||
username: 'admin',
|
||
password: 'admin123'
|
||
},
|
||
permissions: [
|
||
'user:*',
|
||
'role:*',
|
||
'menu:*',
|
||
'config:*',
|
||
'log:read',
|
||
'dict:*'
|
||
],
|
||
cannotAccess: [],
|
||
expectedBehaviors: {
|
||
canCreate: ['user', 'role', 'menu', 'config', 'dict'],
|
||
canRead: ['user', 'role', 'menu', 'config', 'dict', 'log'],
|
||
canUpdate: ['user', 'role', 'menu', 'config', 'dict'],
|
||
canDelete: ['user', 'role', 'menu', 'config', 'dict']
|
||
}
|
||
};
|
||
```
|
||
|
||
#### 1.3 普通用户角色定义
|
||
|
||
```typescript
|
||
// roles/user.role.ts
|
||
export const UserRole: RoleDefinition = {
|
||
name: 'user',
|
||
displayName: '普通用户',
|
||
credentials: {
|
||
username: 'testuser',
|
||
password: 'Test123!@#'
|
||
},
|
||
permissions: [
|
||
'user:read:self',
|
||
'user:update:self'
|
||
],
|
||
cannotAccess: [
|
||
'/user-management',
|
||
'/role-management',
|
||
'/menu-management',
|
||
'/system-config'
|
||
],
|
||
expectedBehaviors: {
|
||
canCreate: [],
|
||
canRead: ['self'],
|
||
canUpdate: ['self'],
|
||
canDelete: []
|
||
}
|
||
};
|
||
```
|
||
|
||
#### 1.4 角色工厂
|
||
|
||
```typescript
|
||
// roles/role-factory.ts
|
||
export class RoleFactory {
|
||
private static roles: Map<string, RoleDefinition> = new Map([
|
||
['admin', AdminRole],
|
||
['user', UserRole],
|
||
['test', TestRole]
|
||
]);
|
||
|
||
static getRole(roleName: string): RoleDefinition {
|
||
const role = this.roles.get(roleName);
|
||
if (!role) {
|
||
throw new Error(`Role '${roleName}' not found`);
|
||
}
|
||
return role;
|
||
}
|
||
|
||
static getAllRoles(): RoleDefinition[] {
|
||
return Array.from(this.roles.values());
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 认证辅助工具
|
||
|
||
#### 2.1 Token管理器
|
||
|
||
```typescript
|
||
// shared/role-auth-manager.ts
|
||
export class RoleAuthManager {
|
||
private static tokenCache: Map<string, {
|
||
token: string;
|
||
expiresAt: number;
|
||
}> = new Map();
|
||
|
||
/**
|
||
* 获取角色Token(带缓存和自动刷新)
|
||
*/
|
||
static async getRoleToken(roleName: string): Promise<string> {
|
||
const cached = this.tokenCache.get(roleName);
|
||
|
||
// 如果Token还有效(提前5分钟刷新)
|
||
if (cached && cached.expiresAt > Date.now() + 300000) {
|
||
return cached.token;
|
||
}
|
||
|
||
// 通过真实API获取Token
|
||
const role = RoleFactory.getRole(roleName);
|
||
const token = await this.fetchTokenFromAPI(role.credentials);
|
||
|
||
return token;
|
||
}
|
||
|
||
/**
|
||
* 从API获取真实Token
|
||
*/
|
||
private static async fetchTokenFromAPI(credentials: {
|
||
username: string;
|
||
password: string
|
||
}): Promise<string> {
|
||
const response = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(credentials)
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
// 缓存Token(24小时有效期)
|
||
this.tokenCache.set(credentials.username, {
|
||
token: data.token,
|
||
expiresAt: Date.now() + 86400000
|
||
});
|
||
|
||
return data.token;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2.2 认证辅助类
|
||
|
||
```typescript
|
||
// shared/auth-helper.ts
|
||
export class AuthHelper {
|
||
/**
|
||
* 以指定角色身份登录(支持两种模式)
|
||
*/
|
||
static async loginAsRole(
|
||
page: Page,
|
||
roleName: string,
|
||
useFullLogin: boolean = false
|
||
): Promise<void> {
|
||
if (useFullLogin) {
|
||
await this.performFullLogin(page, roleName);
|
||
} else {
|
||
await this.injectToken(page, roleName);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 注入Token(用于业务测试,快速高效)
|
||
*/
|
||
private static async injectToken(page: Page, roleName: string): Promise<void> {
|
||
const token = await RoleAuthManager.getRoleToken(roleName);
|
||
|
||
await page.goto('/');
|
||
await page.evaluate((token) => {
|
||
localStorage.setItem('token', token);
|
||
localStorage.setItem('access_token', token);
|
||
}, token);
|
||
|
||
await page.reload();
|
||
}
|
||
|
||
/**
|
||
* 执行完整登录流程(用于认证相关测试)
|
||
*/
|
||
private static async performFullLogin(page: Page, roleName: string): Promise<void> {
|
||
const role = RoleFactory.getRole(roleName);
|
||
const loginPage = new LoginPage(page);
|
||
|
||
await loginPage.goto();
|
||
await loginPage.login(role.credentials.username, role.credentials.password);
|
||
await page.waitForURL(/\/(dashboard|\/)/);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. 测试数据管理器
|
||
|
||
```typescript
|
||
// shared/test-data-manager.ts
|
||
export class TestDataManager {
|
||
private static createdUsers: Set<string> = new Set();
|
||
private static createdRoles: Set<string> = new Set();
|
||
|
||
/**
|
||
* 生成测试用户数据
|
||
*/
|
||
static generateTestUser(overrides?: Partial<TestUserData>): TestUserData {
|
||
const uuid = uuidv4().substring(0, 8);
|
||
return {
|
||
username: `test_${uuid}`,
|
||
password: 'Test123!@#',
|
||
email: `test_${uuid}@example.com`,
|
||
phone: `138${uuid.substring(0, 8)}`,
|
||
nickname: `测试用户_${Date.now()}`,
|
||
...overrides
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 记录创建的用户(用于清理)
|
||
*/
|
||
static trackUser(username: string): void {
|
||
this.createdUsers.add(username);
|
||
}
|
||
|
||
/**
|
||
* 清理所有测试数据
|
||
*/
|
||
static async cleanupAll(page: Page): Promise<void> {
|
||
for (const username of this.createdUsers) {
|
||
await this.deleteUserViaAPI(page, username);
|
||
}
|
||
this.createdUsers.clear();
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4. 权限验证工具
|
||
|
||
```typescript
|
||
// shared/permission-helper.ts
|
||
export class PermissionHelper {
|
||
/**
|
||
* 验证用户可以访问指定路径
|
||
*/
|
||
static async verifyCanAccess(page: Page, path: string): Promise<void> {
|
||
await page.goto(path);
|
||
|
||
// 验证没有跳转到登录页
|
||
await expect(page).not.toHaveURL(/.*login/);
|
||
|
||
// 验证没有显示无权限提示
|
||
const noPermissionElement = page.locator('.no-permission, .forbidden');
|
||
await expect(noPermissionElement).not.toBeVisible();
|
||
}
|
||
|
||
/**
|
||
* 验证用户不能访问指定路径
|
||
*/
|
||
static async verifyCannotAccess(page: Page, path: string): Promise<void> {
|
||
await page.goto(path);
|
||
|
||
const isLoginPage = page.url().includes('login');
|
||
const hasNoPermission = await page.locator('.no-permission').isVisible();
|
||
const hasForbidden = await page.locator('text=/403|Forbidden/').isVisible();
|
||
|
||
expect(isLoginPage || hasNoPermission || hasForbidden).toBeTruthy();
|
||
}
|
||
|
||
/**
|
||
* 验证用户可以看到指定菜单
|
||
*/
|
||
static async verifyCanSeeMenu(page: Page, menuText: string): Promise<void> {
|
||
const menuElement = page.locator(`.menu-item:has-text("${menuText}")`);
|
||
await expect(menuElement).toBeVisible();
|
||
}
|
||
|
||
/**
|
||
* 验证用户看不到指定菜单
|
||
*/
|
||
static async verifyCannotSeeMenu(page: Page, menuText: string): Promise<void> {
|
||
const menuElement = page.locator(`.menu-item:has-text("${menuText}")`);
|
||
await expect(menuElement).not.toBeVisible();
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 测试场景实现
|
||
|
||
### 1. 认证场景测试(真实登录)
|
||
|
||
```typescript
|
||
// scenarios/authentication/login-flow.spec.ts
|
||
test.describe('认证流程测试', () => {
|
||
test('管理员使用正确凭证登录成功', async ({ page }) => {
|
||
// 使用真实登录流程
|
||
await AuthHelper.loginAsRole(page, 'admin', true);
|
||
|
||
// 验证登录成功
|
||
await expect(page).toHaveURL(/\/(dashboard|\/)/);
|
||
const isLoggedIn = await AuthHelper.isLoggedIn(page);
|
||
expect(isLoggedIn).toBeTruthy();
|
||
});
|
||
|
||
test('管理员使用错误密码登录失败', async ({ page }) => {
|
||
const role = RoleFactory.getRole('admin');
|
||
|
||
await page.goto('/login');
|
||
await page.fill('[name="username"]', role.credentials.username);
|
||
await page.fill('[name="password"]', 'wrongpassword');
|
||
await page.click('[type="submit"]');
|
||
|
||
await expect(page).toHaveURL(/.*login/);
|
||
await expect(page.locator('.error-message')).toBeVisible();
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 用户管理场景测试(Token注入)
|
||
|
||
```typescript
|
||
// scenarios/user-management/admin-creates-user.spec.ts
|
||
test.describe('管理员创建用户场景', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
// 使用Token注入快速登录
|
||
await AuthHelper.loginAsRole(page, 'admin');
|
||
});
|
||
|
||
test.afterEach(async ({ page }) => {
|
||
// 清理测试数据
|
||
await TestDataManager.cleanupAll(page);
|
||
});
|
||
|
||
test('管理员创建新用户成功', async ({ page }) => {
|
||
const testUser = TestDataManager.generateTestUser();
|
||
|
||
await userManagementPage.goto();
|
||
await userManagementPage.clickCreateUser();
|
||
await userManagementPage.fillUserForm(testUser);
|
||
await userManagementPage.submitForm();
|
||
|
||
const success = await userManagementPage.waitForSuccessMessage();
|
||
expect(success).toBeTruthy();
|
||
|
||
TestDataManager.trackUser(testUser.username);
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
### 3. 权限边界验证测试
|
||
|
||
```typescript
|
||
// scenarios/user-management/permission-boundary.spec.ts
|
||
test.describe('用户管理权限边界验证', () => {
|
||
test('普通用户无法访问用户管理页面', async ({ page }) => {
|
||
await AuthHelper.loginAsRole(page, 'user');
|
||
await PermissionHelper.verifyCannotAccess(page, '/user-management');
|
||
});
|
||
|
||
test('普通用户无法看到用户管理菜单', async ({ page }) => {
|
||
await AuthHelper.loginAsRole(page, 'user');
|
||
await PermissionHelper.verifyCannotSeeMenu(page, '用户管理');
|
||
});
|
||
|
||
test('普通用户无法创建用户', async ({ page }) => {
|
||
await AuthHelper.loginAsRole(page, 'user');
|
||
|
||
const token = await page.evaluate(() => localStorage.getItem('token'));
|
||
const response = await fetch(`${API_BASE_URL}/api/users`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ username: 'hacker', password: 'hack123' })
|
||
});
|
||
|
||
expect(response.status).toBe(403);
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
### 4. 用户生命周期完整场景
|
||
|
||
```typescript
|
||
// scenarios/user-management/user-lifecycle.spec.ts
|
||
test.describe('用户完整生命周期测试', () => {
|
||
test('阶段1: 管理员创建用户', async ({ page }) => {
|
||
await AuthHelper.loginAsRole(page, 'admin');
|
||
await userManagementPage.goto();
|
||
await userManagementPage.clickCreateUser();
|
||
await userManagementPage.fillUserForm(testUser);
|
||
await userManagementPage.submitForm();
|
||
|
||
expect(await userManagementPage.waitForSuccessMessage()).toBeTruthy();
|
||
TestDataManager.trackUser(testUser.username);
|
||
});
|
||
|
||
test('阶段2: 新用户首次登录', async ({ page }) => {
|
||
await loginPage.goto();
|
||
await loginPage.login(testUser.username, testUser.password);
|
||
await expect(page).toHaveURL(/\/(dashboard|\/)/);
|
||
await expect(page.locator('text=用户管理')).not.toBeVisible();
|
||
});
|
||
|
||
test('阶段3: 用户修改个人信息', async ({ page }) => {
|
||
await AuthHelper.loginAsRole(page, 'user');
|
||
await page.click('.user-avatar');
|
||
await page.click('text=个人中心');
|
||
await page.fill('[name="nickname"]', '更新昵称');
|
||
await page.click('[type="submit"]');
|
||
await expect(page.locator('.success-message')).toBeVisible();
|
||
});
|
||
|
||
test('阶段4: 管理员禁用用户', async ({ page }) => {
|
||
await AuthHelper.loginAsRole(page, 'admin');
|
||
await userManagementPage.goto();
|
||
await userManagementPage.search(testUser.username);
|
||
await userManagementPage.clickStatusButton(1);
|
||
expect(await userManagementPage.waitForSuccessMessage()).toBeTruthy();
|
||
});
|
||
|
||
test('阶段5: 禁用用户无法登录', async ({ page }) => {
|
||
await loginPage.goto();
|
||
await loginPage.login(testUser.username, testUser.password);
|
||
await expect(page).toHaveURL(/.*login/);
|
||
await expect(page.locator('.error-message')).toBeVisible();
|
||
});
|
||
|
||
test('阶段6: 管理员删除用户', async ({ page }) => {
|
||
await AuthHelper.loginAsRole(page, 'admin');
|
||
await userManagementPage.goto();
|
||
await userManagementPage.search(testUser.username);
|
||
await userManagementPage.clickDeleteButton(1);
|
||
await userManagementPage.confirmDelete();
|
||
expect(await userManagementPage.waitForSuccessMessage()).toBeTruthy();
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 配置和CI/CD集成
|
||
|
||
### 1. Playwright配置
|
||
|
||
```typescript
|
||
// playwright.config.ts
|
||
export default defineConfig({
|
||
testDir: './e2e',
|
||
testMatch: [
|
||
'**/role-based-tests/**/*.spec.ts',
|
||
'**/legacy-tests/**/*.spec.ts'
|
||
],
|
||
timeout: 30000,
|
||
retries: process.env.CI ? 2 : 0,
|
||
workers: process.env.CI ? 1 : undefined,
|
||
reporter: [
|
||
['list'],
|
||
['html', { outputFolder: 'test-results/html' }],
|
||
['junit', { outputFile: 'test-results/junit.xml' }]
|
||
],
|
||
projects: [
|
||
{
|
||
name: 'admin-tests',
|
||
testMatch: /admin.*\.spec\.ts/,
|
||
},
|
||
{
|
||
name: 'user-tests',
|
||
testMatch: /user.*\.spec\.ts/,
|
||
},
|
||
{
|
||
name: 'auth-tests',
|
||
testMatch: /authentication.*\.spec\.ts/,
|
||
},
|
||
],
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 环境变量配置
|
||
|
||
```bash
|
||
# .env.test
|
||
VITE_API_BASE_URL=http://localhost:8084
|
||
BASE_URL=http://localhost:5173
|
||
TEST_TIMEOUT=30000
|
||
TEST_RETRIES=2
|
||
ADMIN_USERNAME=admin
|
||
ADMIN_PASSWORD=admin123
|
||
USER_USERNAME=testuser
|
||
USER_PASSWORD=Test123!@#
|
||
```
|
||
|
||
---
|
||
|
||
### 3. Jenkins Pipeline配置
|
||
|
||
```groovy
|
||
// Jenkinsfile
|
||
pipeline {
|
||
agent {
|
||
label 'node18-chrome'
|
||
}
|
||
|
||
environment {
|
||
ADMIN_PASSWORD = credentials('admin-password')
|
||
VITE_API_BASE_URL = 'http://localhost:8084'
|
||
}
|
||
|
||
stages {
|
||
stage('准备环境') {
|
||
steps {
|
||
sh '''
|
||
cd novalon-manage-web
|
||
pnpm install
|
||
pnpm exec playwright install chromium
|
||
'''
|
||
}
|
||
}
|
||
|
||
stage('运行基于角色的测试套件') {
|
||
parallel {
|
||
stage('管理员角色测试') {
|
||
steps {
|
||
sh 'cd novalon-manage-web && pnpm test:admin'
|
||
}
|
||
}
|
||
|
||
stage('普通用户角色测试') {
|
||
steps {
|
||
sh 'cd novalon-manage-web && pnpm test:user'
|
||
}
|
||
}
|
||
|
||
stage('认证流程测试') {
|
||
steps {
|
||
sh 'cd novalon-manage-web && pnpm test:auth'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
post {
|
||
always {
|
||
junit 'novalon-manage-web/test-results/junit.xml'
|
||
publishHTML(target: [
|
||
reportDir: 'novalon-manage-web/test-results/html',
|
||
reportFiles: 'index.html',
|
||
reportName: 'Playwright测试报告'
|
||
])
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 实施计划
|
||
|
||
### 阶段1:基础设施搭建(第1周)
|
||
|
||
**目标**:建立测试框架基础
|
||
|
||
**任务清单**:
|
||
- [ ] **修复H2数据库密码不一致问题**(优先级:P0)
|
||
- [ ] 统一主应用和测试环境的data-h2.sql密码配置
|
||
- [ ] 验证BCrypt版本兼容性
|
||
- [ ] 更新角色定义文件中的密码
|
||
- [ ] 添加密码验证测试
|
||
- [ ] 创建目录结构
|
||
- [ ] 实现角色定义系统
|
||
- [ ] 实现核心工具类
|
||
- [ ] 配置环境变量和Playwright配置
|
||
- [ ] 编写单元测试验证工具类
|
||
|
||
**验收标准**:
|
||
- ✅ **密码配置一致且验证通过**
|
||
- ✅ 所有工具类单元测试通过
|
||
- ✅ Token获取和注入功能正常
|
||
- ✅ 角色定义完整且可扩展
|
||
|
||
---
|
||
|
||
### 阶段2:核心场景迁移(第2-3周)
|
||
|
||
**目标**:迁移高优先级测试场景
|
||
|
||
**P0 - 认证场景(第2周前半)**:
|
||
- [ ] login-flow.spec.ts
|
||
- [ ] logout-flow.spec.ts
|
||
- [ ] permission-validation.spec.ts
|
||
|
||
**P1 - 用户管理场景(第2周后半)**:
|
||
- [ ] admin-creates-user.spec.ts
|
||
- [ ] user-edits-profile.spec.ts
|
||
- [ ] user-lifecycle.spec.ts
|
||
- [ ] permission-boundary.spec.ts
|
||
|
||
**P2 - 角色管理场景(第3周前半)**:
|
||
- [ ] admin-manages-roles.spec.ts
|
||
- [ ] permission-assignment.spec.ts
|
||
|
||
**P3 - 菜单管理场景(第3周后半)**:
|
||
- [ ] admin-manages-menus.spec.ts
|
||
|
||
**验收标准**:
|
||
- ✅ 每个场景测试通过率100%
|
||
- ✅ 测试覆盖率不低于旧测试
|
||
- ✅ 执行时间在可接受范围内
|
||
|
||
---
|
||
|
||
### 阶段3:验证和优化(第4周)
|
||
|
||
**目标**:确保质量并优化性能
|
||
|
||
**任务清单**:
|
||
- [ ] 全量运行新测试套件
|
||
- [ ] 对比新旧测试覆盖率
|
||
- [ ] 性能基准测试
|
||
- [ ] 跨浏览器兼容性测试
|
||
- [ ] 文档完善
|
||
|
||
**验收标准**:
|
||
- ✅ 测试覆盖率 ≥ 旧测试覆盖率
|
||
- ✅ 平均执行时间 ≤ 旧测试执行时间 * 0.7
|
||
- ✅ 所有浏览器测试通过
|
||
|
||
---
|
||
|
||
### 阶段4:清理和扩展(第5周及以后)
|
||
|
||
**目标**:清理旧测试并持续改进
|
||
|
||
**任务清单**:
|
||
- [ ] 删除已迁移的旧测试文件
|
||
- [ ] 保留边缘场景测试
|
||
- [ ] 建立测试维护流程
|
||
|
||
**验收标准**:
|
||
- ✅ 无重复测试
|
||
- ✅ 测试套件结构清晰
|
||
|
||
---
|
||
|
||
## 风险控制
|
||
|
||
### 风险1:新测试遗漏关键场景
|
||
|
||
**预防措施**:
|
||
- 迁移前详细分析旧测试
|
||
- 使用覆盖率工具对比
|
||
- Code Review重点检查场景完整性
|
||
|
||
**回滚策略**:
|
||
```bash
|
||
git revert <commit-hash>
|
||
git checkout <old-commit> -- e2e/old-test.spec.ts
|
||
```
|
||
|
||
---
|
||
|
||
### 风险2:Token注入失败
|
||
|
||
**预防措施**:
|
||
- 实现Token缓存和自动刷新
|
||
- 添加降级机制
|
||
|
||
**降级代码**:
|
||
```typescript
|
||
static async loginAsRole(page: Page, roleName: string, useFullLogin = false) {
|
||
if (useFullLogin) {
|
||
await this.performFullLogin(page, roleName);
|
||
} else {
|
||
try {
|
||
await this.injectToken(page, roleName);
|
||
} catch (error) {
|
||
console.warn('Token注入失败,降级使用真实登录');
|
||
await this.performFullLogin(page, roleName);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 风险3:测试数据污染
|
||
|
||
**预防措施**:
|
||
- 使用独立的测试数据库
|
||
- 每个测试后强制清理数据
|
||
- 定期重置测试环境
|
||
|
||
**清理脚本**:
|
||
```bash
|
||
#!/bin/bash
|
||
psql -U novalon -d novalon_manage_test -c "TRUNCATE users, roles CASCADE;"
|
||
psql -U novalon -d novalon_manage_test -f db/migration/V2__Insert_initial_data.sql
|
||
```
|
||
|
||
---
|
||
|
||
### 风险4:H2数据库密码不一致问题 ⚠️
|
||
|
||
**问题描述**:
|
||
|
||
当前系统存在两个data-h2.sql文件,密码配置不一致:
|
||
|
||
| 文件位置 | BCrypt版本 | 密码Hash | 明文密码 |
|
||
|---------|-----------|---------|---------|
|
||
| `manage-app/src/main/resources/data-h2.sql` | `$2b$` | `SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy` | `admin123` |
|
||
| `manage-app/src/test/resources/data-h2.sql` | `$2a$` | `nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C` | `Test@123` |
|
||
|
||
**根本原因**:
|
||
1. **BCrypt版本不一致**:主应用用`$2b$`,测试环境用`$2a$`
|
||
2. **密码不一致**:主应用用`admin123`,测试环境用`Test@123`
|
||
3. **Hash不一致**:两个完全不同的hash
|
||
4. **可能导致**:测试环境登录失败,或密码验证失败
|
||
|
||
**解决方案**:
|
||
|
||
**方案A:统一使用测试环境配置(推荐)**
|
||
|
||
1. **统一密码**:所有环境使用`Test@123`作为测试密码
|
||
2. **统一BCrypt版本**:使用`$2a$`(Spring Security BCryptPasswordEncoder默认版本)
|
||
3. **更新主应用data-h2.sql**:
|
||
|
||
```sql
|
||
-- 插入测试用户
|
||
-- BCrypt哈希值对应明文密码: Test@123
|
||
INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by)
|
||
VALUES
|
||
(1, 'admin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system'),
|
||
(2, 'testadmin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'testadmin@novalon.com', '13800138001', '测试管理员', 1, 'system', 'system'),
|
||
(3, 'normaluser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'normaluser@novalon.com', '13800138002', '普通用户', 1, 'system', 'system'),
|
||
(4, 'guestuser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'guestuser@novalon.com', '13800138003', '访客用户', 1, 'system', 'system'),
|
||
(5, 'disableduser', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'disableduser@novalon.com', '13800138004', '禁用用户', 0, 'system', 'system'),
|
||
(10, 'e2e_test_user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'e2e@test.com', '13900139000', 'E2E测试用户', 1, 'system', 'system');
|
||
```
|
||
|
||
4. **更新角色定义文件**:
|
||
|
||
```typescript
|
||
// roles/admin.role.ts
|
||
export const AdminRole: RoleDefinition = {
|
||
name: 'admin',
|
||
displayName: '超级管理员',
|
||
credentials: {
|
||
username: 'admin',
|
||
password: 'Test@123' // 统一使用Test@123
|
||
},
|
||
// ...
|
||
};
|
||
|
||
// roles/user.role.ts
|
||
export const UserRole: RoleDefinition = {
|
||
name: 'user',
|
||
displayName: '普通用户',
|
||
credentials: {
|
||
username: 'normaluser',
|
||
password: 'Test@123' // 统一使用Test@123
|
||
},
|
||
// ...
|
||
};
|
||
```
|
||
|
||
**方案B:生成新的统一密码Hash**
|
||
|
||
使用Spring Security的BCryptPasswordEncoder生成新的hash:
|
||
|
||
```java
|
||
@Test
|
||
public void generateUnifiedPasswordHash() {
|
||
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(12);
|
||
|
||
String password = "Test@123";
|
||
String hash = passwordEncoder.encode(password);
|
||
|
||
System.out.println("密码: " + password);
|
||
System.out.println("哈希: " + hash);
|
||
|
||
// 验证
|
||
boolean matches = passwordEncoder.matches(password, hash);
|
||
System.out.println("验证结果: " + matches);
|
||
}
|
||
```
|
||
|
||
**验证步骤**:
|
||
|
||
1. **验证BCrypt版本兼容性**:
|
||
|
||
```java
|
||
@Test
|
||
public void verifyBCryptVersions() {
|
||
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(12);
|
||
|
||
String password = "Test@123";
|
||
|
||
// $2a$ hash
|
||
String hash2a = "$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C";
|
||
boolean matches2a = passwordEncoder.matches(password, hash2a);
|
||
System.out.println("$2a$ hash验证: " + matches2a);
|
||
|
||
// $2b$ hash
|
||
String hash2b = "$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy";
|
||
boolean matches2b = passwordEncoder.matches(password, hash2b);
|
||
System.out.println("$2b$ hash验证: " + matches2b);
|
||
}
|
||
```
|
||
|
||
2. **验证登录流程**:
|
||
|
||
```typescript
|
||
test('验证统一密码配置', async ({ page }) => {
|
||
await AuthHelper.loginAsRole(page, 'admin', true);
|
||
await expect(page).toHaveURL(/\/(dashboard|\/)/);
|
||
});
|
||
```
|
||
|
||
**预防措施**:
|
||
- 在实施计划第一阶段立即修复此问题
|
||
- 添加测试验证密码配置的一致性
|
||
- 在CI/CD中添加密码验证步骤
|
||
|
||
**影响范围**:
|
||
- ✅ 所有使用H2数据库的测试
|
||
- ✅ 所有角色定义文件
|
||
- ✅ 所有认证相关测试
|
||
|
||
---
|
||
|
||
## 成功指标
|
||
|
||
### 质量指标
|
||
|
||
| 指标 | 目标值 | 测量方法 |
|
||
|------|--------|----------|
|
||
| 测试覆盖率 | ≥ 80% | Jest coverage report |
|
||
| 测试通过率 | 100% | CI构建结果 |
|
||
| 缺陷发现率 | 提升20% | Bug统计对比 |
|
||
| 误报率 | < 5% | Flaky test监控 |
|
||
|
||
---
|
||
|
||
### 效率指标
|
||
|
||
| 指标 | 目标值 | 测量方法 |
|
||
|------|--------|----------|
|
||
| 执行时间 | ≤ 旧测试 * 0.7 | CI执行时间统计 |
|
||
| 维护成本 | 降低30% | 代码变更频率 |
|
||
| 新测试编写时间 | < 30分钟/场景 | 开发者反馈 |
|
||
|
||
---
|
||
|
||
### 业务指标
|
||
|
||
| 指标 | 目标值 | 测量方法 |
|
||
|------|--------|----------|
|
||
| 权限bug发现 | ≥ 5个 | Bug分类统计 |
|
||
| 回归测试覆盖 | 100%核心场景 | 场景清单检查 |
|
||
| UAT通过率 | ≥ 95% | UAT结果统计 |
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
### 核心优势
|
||
|
||
1. **真实性保障**:混合模式确保业务流程和权限验证的真实性
|
||
2. **执行效率**:Token注入节省约37%执行时间
|
||
3. **可维护性**:清晰的角色定义和工具类分层
|
||
4. **可扩展性**:易于添加新角色和新场景
|
||
5. **风险可控**:渐进式迁移,随时可回滚
|
||
|
||
---
|
||
|
||
### 预期收益
|
||
|
||
- 🎯 **测试覆盖率提升**:从当前分散测试到系统化场景覆盖
|
||
- ⚡ **执行效率提升**:节省约37%执行时间
|
||
- 🐛 **缺陷发现能力提升**:权限边界验证增强
|
||
- 📊 **可维护性提升**:清晰的结构和工具化支持
|
||
- 🚀 **开发效率提升**:新测试编写时间 < 30分钟
|
||
|
||
---
|
||
|
||
## 附录
|
||
|
||
### 参考资料
|
||
|
||
- [Playwright最佳实践](https://playwright.dev/docs/best-practices)
|
||
- [RBAC权限模型设计](https://en.wikipedia.org/wiki/Role-based_access_control)
|
||
- [测试金字塔理论](https://martinfowler.com/articles/practical-test-pyramid.html)
|
||
|
||
---
|
||
|
||
**文档版本历史**:
|
||
- v1.0 (2026-04-04): 初始版本
|