Files
gym-manage/docs/superpowers/specs/2026-04-04-role-based-test-suite-design.md

35 KiB
Raw Permalink Blame History

基于角色的用户模拟测试套件设计方案

版本: 1.0
日期: 2026-04-04
作者: 张翔
状态: 待审查


目录

  1. 概述
  2. 核心决策
  3. 整体架构设计
  4. 核心组件设计
  5. 测试场景实现
  6. 配置和CI/CD集成
  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最佳实践:完美契合"谁在什么场景下能做什么"的核心思想

示例

// 业务流程测试
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%总时间

决策7CI/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 角色基类

// 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 管理员角色定义

// 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 普通用户角色定义

// 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 角色工厂

// 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管理器

// 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();
    
    // 缓存Token24小时有效期)
    this.tokenCache.set(credentials.username, {
      token: data.token,
      expiresAt: Date.now() + 86400000
    });

    return data.token;
  }
}

2.2 认证辅助类

// 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. 测试数据管理器

// 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. 权限验证工具

// 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. 认证场景测试(真实登录)

// 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注入)

// 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. 权限边界验证测试

// 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. 用户生命周期完整场景

// 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配置

// 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. 环境变量配置

# .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配置

// 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重点检查场景完整性

回滚策略

git revert <commit-hash>
git checkout <old-commit> -- e2e/old-test.spec.ts

风险2Token注入失败

预防措施

  • 实现Token缓存和自动刷新
  • 添加降级机制

降级代码

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:测试数据污染

预防措施

  • 使用独立的测试数据库
  • 每个测试后强制清理数据
  • 定期重置测试环境

清理脚本

#!/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

风险4H2数据库密码不一致问题 ⚠️

问题描述

当前系统存在两个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
-- 插入测试用户
-- 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');
  1. 更新角色定义文件
// 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

@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版本兼容性
@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);
}
  1. 验证登录流程
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分钟

附录

参考资料


文档版本历史

  • v1.0 (2026-04-04): 初始版本