08ea5fbe98
添加用户管理视图、API和状态管理文件
734 lines
17 KiB
Markdown
734 lines
17 KiB
Markdown
# E2E测试工具使用指南
|
||
|
||
## 概述
|
||
|
||
本E2E测试工具是一个基于Playwright的可复用端到端测试框架,提供了模块化的测试用例编写能力、统一的测试环境配置、常用测试操作的封装与复用,以及清晰的测试报告与日志输出能力。
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
e2e/
|
||
├── core/ # 核心模块
|
||
│ ├── test-config.ts # 测试配置管理
|
||
│ ├── test-data.ts # 测试数据生成器
|
||
│ ├── test-logger.ts # 测试日志记录器
|
||
│ └── test-reporter.ts # 测试报告生成器
|
||
├── pages/ # 页面对象模型
|
||
│ ├── base-page.ts # 基础页面类
|
||
│ ├── login-page.ts # 登录页面
|
||
│ ├── dashboard-page.ts # 仪表盘页面
|
||
│ ├── user-management-page.ts # 用户管理页面
|
||
│ ├── role-management-page.ts # 角色管理页面
|
||
│ └── menu-management-page.ts # 菜单管理页面
|
||
├── helpers/ # 测试辅助工具
|
||
│ ├── screenshot-helper.ts # 截图辅助工具
|
||
│ ├── form-helper.ts # 表单辅助工具
|
||
│ └── table-helper.ts # 表格辅助工具
|
||
├── fixtures/ # 测试夹具
|
||
├── utils/ # 工具函数
|
||
│ └── common-utils.ts # 通用工具函数
|
||
├── constants/ # 常量定义
|
||
│ └── index.ts # 常量集合
|
||
├── examples/ # 示例测试
|
||
│ └── complete-example.spec.ts
|
||
├── test-fixtures.ts # Playwright测试夹具
|
||
└── mock-manager.ts # Mock服务管理器
|
||
```
|
||
|
||
## 快速开始
|
||
|
||
### 1. 安装依赖
|
||
|
||
```bash
|
||
npm install --save-dev @playwright/test
|
||
```
|
||
|
||
### 2. 配置测试环境
|
||
|
||
在项目根目录创建 `.env.e2e` 文件:
|
||
|
||
```env
|
||
# E2E测试环境配置
|
||
E2E_ENV=local
|
||
E2E_BASE_URL=http://localhost:5173
|
||
E2E_MOCK_ENABLED=true
|
||
E2E_MOCK_MODE=full
|
||
```
|
||
|
||
### 3. 编写测试用例
|
||
|
||
使用提供的测试夹具编写测试用例:
|
||
|
||
```typescript
|
||
import { test, expect } from './test-fixtures';
|
||
|
||
test.describe('登录功能测试', () => {
|
||
test('成功登录', async ({ pageObjects, testData, testLogger }) => {
|
||
testLogger.startTest('成功登录');
|
||
|
||
try {
|
||
await pageObjects.loginPage.navigate();
|
||
await pageObjects.loginPage.login(testData.admin.username, testData.admin.password);
|
||
|
||
await pageObjects.dashboardPage.waitForLoad();
|
||
|
||
const pageTitle = await pageObjects.dashboardPage.getPageTitle();
|
||
expect(pageTitle).toContain('仪表盘');
|
||
|
||
testLogger.endTest('成功登录', 'passed');
|
||
} catch (error) {
|
||
testLogger.endTest('成功登录', 'failed', error as Error);
|
||
throw error;
|
||
}
|
||
});
|
||
});
|
||
```
|
||
|
||
### 4. 运行测试
|
||
|
||
```bash
|
||
# 运行所有测试
|
||
npm run test:e2e
|
||
|
||
# 运行特定测试文件
|
||
npx playwright test e2e/examples/complete-example.spec.ts
|
||
|
||
# 运行特定测试用例
|
||
npx playwright test -g "成功登录"
|
||
|
||
# 调试模式运行
|
||
npx playwright test --debug
|
||
```
|
||
|
||
## 核心功能
|
||
|
||
### 1. 测试配置管理
|
||
|
||
`test-config.ts` 提供了统一的测试环境配置管理:
|
||
|
||
```typescript
|
||
import { testConfig } from './core/test-config';
|
||
|
||
// 获取当前环境配置
|
||
const env = testConfig.getEnvironment();
|
||
console.log(env.name); // 环境名称
|
||
console.log(env.baseURL); // 基础URL
|
||
console.log(env.mockEnabled); // Mock是否启用
|
||
console.log(env.timeout); // 超时配置
|
||
|
||
// 切换环境
|
||
testConfig.setEnvironment('dev');
|
||
|
||
// 获取特定环境配置
|
||
const devConfig = testConfig.getEnvironment('dev');
|
||
```
|
||
|
||
### 2. 测试数据生成
|
||
|
||
`test-data.ts` 提供了测试数据生成器:
|
||
|
||
```typescript
|
||
import { testDataGenerator } from './core/test-data';
|
||
|
||
// 生成用户数据
|
||
const userData = testDataGenerator.generateUserData({
|
||
username: 'testuser',
|
||
email: 'test@example.com',
|
||
status: 'active'
|
||
});
|
||
|
||
// 生成角色数据
|
||
const roleData = testDataGenerator.generateRoleData({
|
||
roleName: '测试角色',
|
||
roleCode: 'test_role',
|
||
status: 1
|
||
});
|
||
|
||
// 生成菜单数据
|
||
const menuData = testDataGenerator.generateMenuData({
|
||
menuName: '测试菜单',
|
||
menuType: 1,
|
||
path: '/test',
|
||
status: 0
|
||
});
|
||
|
||
// 生成权限数据
|
||
const permissionData = testDataGenerator.generatePermissionData({
|
||
permissionName: '测试权限',
|
||
permissionCode: 'test:permission',
|
||
permissionType: 'button'
|
||
});
|
||
```
|
||
|
||
### 3. 测试日志记录
|
||
|
||
`test-logger.ts` 提供了结构化的测试日志记录:
|
||
|
||
```typescript
|
||
import { testLogger } from './core/test-logger';
|
||
|
||
// 开始测试
|
||
testLogger.startTest('测试名称');
|
||
|
||
// 开始测试步骤
|
||
testLogger.startStep('步骤名称');
|
||
|
||
// 记录不同级别的日志
|
||
testLogger.debug('调试信息');
|
||
testLogger.info('普通信息');
|
||
testLogger.warn('警告信息');
|
||
testLogger.error('错误信息', error);
|
||
|
||
// 结束测试步骤
|
||
testLogger.endStep('步骤名称', 'passed');
|
||
|
||
// 结束测试
|
||
testLogger.endTest('测试名称', 'passed');
|
||
```
|
||
|
||
### 4. 测试报告生成
|
||
|
||
`test-reporter.ts` 提供了测试报告生成功能:
|
||
|
||
```typescript
|
||
import { testReporter } from './core/test-reporter';
|
||
|
||
// 开始测试报告
|
||
testReporter.startReport();
|
||
|
||
// 记录测试结果
|
||
testReporter.recordTestResult({
|
||
testName: '测试名称',
|
||
status: 'passed',
|
||
duration: 1000,
|
||
steps: [],
|
||
logs: [],
|
||
screenshots: [],
|
||
errors: []
|
||
});
|
||
|
||
// 生成所有报告
|
||
await testReporter.generateAllReports('./test-results/reports');
|
||
|
||
// 生成JSON报告
|
||
await testReporter.generateJSONReport('./test-results/reports/e2e-report.json');
|
||
|
||
// 生成HTML报告
|
||
await testReporter.generateHTMLReport('./test-results/reports/e2e-report.html');
|
||
```
|
||
|
||
### 5. 页面对象模型
|
||
|
||
所有页面类都继承自 `BasePage`,提供统一的页面操作接口:
|
||
|
||
```typescript
|
||
import { BasePage } from './pages/base-page';
|
||
import { LoginPage } from './pages/login-page';
|
||
import { DashboardPage } from './pages/dashboard-page';
|
||
|
||
// 使用页面对象
|
||
const loginPage = new LoginPage(page);
|
||
await loginPage.navigate();
|
||
await loginPage.login('admin', 'password');
|
||
|
||
const dashboardPage = new DashboardPage(page);
|
||
await dashboardPage.waitForLoad();
|
||
const title = await dashboardPage.getPageTitle();
|
||
```
|
||
|
||
### 6. 测试辅助工具
|
||
|
||
#### 截图辅助工具
|
||
|
||
```typescript
|
||
import { ScreenshotHelper } from './helpers/screenshot-helper';
|
||
|
||
const screenshotHelper = new ScreenshotHelper(page);
|
||
|
||
// 截取当前页面
|
||
await screenshotHelper.takeScreenshot('page-screenshot');
|
||
|
||
// 截取整个页面
|
||
await screenshotHelper.takeFullPageScreenshot('full-page');
|
||
|
||
// 截取特定元素
|
||
await screenshotHelper.takeElementScreenshot('element-screenshot', '.selector');
|
||
|
||
// 在测试失败时自动截图
|
||
await screenshotHelper.takeScreenshotOnFailure('test-failure');
|
||
```
|
||
|
||
#### 表单辅助工具
|
||
|
||
```typescript
|
||
import { FormHelper } from './helpers/form-helper';
|
||
|
||
const formHelper = new FormHelper(page);
|
||
|
||
// 填写表单字段
|
||
await formHelper.fillField('input[name="username"]', 'testuser');
|
||
await formHelper.fillField('input[type="password"]', 'password', 'password');
|
||
await formHelper.fillField('select[name="role"]', 'admin', 'select');
|
||
await formHelper.fillField('input[type="checkbox"]', true, 'checkbox');
|
||
|
||
// 填写整个表单
|
||
await formHelper.fillForm({
|
||
username: 'testuser',
|
||
password: 'password',
|
||
email: 'test@example.com',
|
||
role: 'admin'
|
||
});
|
||
|
||
// 提交表单
|
||
await formHelper.submitForm();
|
||
|
||
// 重置表单
|
||
await formHelper.resetForm();
|
||
|
||
// 验证表单
|
||
const isValid = await formHelper.validateForm();
|
||
```
|
||
|
||
#### 表格辅助工具
|
||
|
||
```typescript
|
||
import { TableHelper } from './helpers/table-helper';
|
||
|
||
const tableHelper = new TableHelper(page);
|
||
|
||
// 获取表格行数
|
||
const rowCount = await tableHelper.getRowCount('.ant-table');
|
||
|
||
// 获取表格列数
|
||
const columnCount = await tableHelper.getColumnCount('.ant-table');
|
||
|
||
// 获取单元格文本
|
||
const cellText = await tableHelper.getCellText('.ant-table', 0, 0);
|
||
|
||
// 获取整行数据
|
||
const rowData = await tableHelper.getRowData('.ant-table', 0);
|
||
|
||
// 获取整列数据
|
||
const columnData = await tableHelper.getColumnData('.ant-table', 0);
|
||
|
||
// 点击表格行
|
||
await tableHelper.clickRow('.ant-table', 0);
|
||
|
||
// 点击表格单元格
|
||
await tableHelper.clickCell('.ant-table', 0, 0);
|
||
|
||
// 等待表格加载
|
||
await tableHelper.waitForTableLoad('.ant-table');
|
||
|
||
// 验证表格数据
|
||
const isValid = await tableHelper.validateTableData('.ant-table', expectedData);
|
||
```
|
||
|
||
### 7. Mock服务集成
|
||
|
||
```typescript
|
||
import { MockManager } from './mock-manager';
|
||
|
||
const mockConfig = {
|
||
enabled: true,
|
||
mode: 'full',
|
||
mockPaths: [],
|
||
delay: 0,
|
||
logCalls: true,
|
||
validateResponses: true,
|
||
dataSource: 'memory'
|
||
};
|
||
|
||
const mockManager = new MockManager(mockConfig);
|
||
|
||
// 拦截API请求
|
||
await mockManager.interceptAPIRequest(page);
|
||
|
||
// 添加Mock响应
|
||
mockManager.addMockResponse({
|
||
url: '/api/login',
|
||
method: 'POST',
|
||
response: {
|
||
code: 200,
|
||
data: {
|
||
token: 'mock-token',
|
||
userInfo: {
|
||
id: 1,
|
||
username: 'admin'
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 清除Mock响应
|
||
mockManager.clearMockResponses();
|
||
```
|
||
|
||
## 测试夹具
|
||
|
||
本工具提供了以下测试夹具:
|
||
|
||
### pageObjects
|
||
|
||
提供所有页面对象的实例:
|
||
|
||
```typescript
|
||
test('使用页面对象', async ({ pageObjects }) => {
|
||
await pageObjects.loginPage.navigate();
|
||
await pageObjects.loginPage.login('admin', 'password');
|
||
await pageObjects.dashboardPage.waitForLoad();
|
||
});
|
||
```
|
||
|
||
### helpers
|
||
|
||
提供所有辅助工具的实例:
|
||
|
||
```typescript
|
||
test('使用辅助工具', async ({ helpers }) => {
|
||
await helpers.screenshot.takeScreenshot('test');
|
||
await helpers.form.fillField('input[name="username"]', 'test');
|
||
const rowCount = await helpers.table.getRowCount('.ant-table');
|
||
});
|
||
```
|
||
|
||
### testData
|
||
|
||
提供预定义的测试数据:
|
||
|
||
```typescript
|
||
test('使用测试数据', async ({ testData }) => {
|
||
console.log(testData.user); // 普通用户数据
|
||
console.log(testData.admin); // 管理员数据
|
||
console.log(testData.role); // 角色数据
|
||
console.log(testData.menu); // 菜单数据
|
||
console.log(testData.permission); // 权限数据
|
||
});
|
||
```
|
||
|
||
### mockManager
|
||
|
||
提供Mock服务管理器:
|
||
|
||
```typescript
|
||
test('使用Mock服务', async ({ mockManager }) => {
|
||
mockManager.addMockResponse({
|
||
url: '/api/test',
|
||
method: 'GET',
|
||
response: { code: 200, data: 'mock data' }
|
||
});
|
||
});
|
||
```
|
||
|
||
### testConfig
|
||
|
||
提供测试配置:
|
||
|
||
```typescript
|
||
test('使用测试配置', async ({ testConfig }) => {
|
||
console.log(testConfig.name); // 环境名称
|
||
console.log(testConfig.baseURL); // 基础URL
|
||
console.log(testConfig.mockEnabled); // Mock是否启用
|
||
});
|
||
```
|
||
|
||
### testLogger
|
||
|
||
提供测试日志记录器:
|
||
|
||
```typescript
|
||
test('使用测试日志', async ({ testLogger }) => {
|
||
testLogger.info('测试信息');
|
||
testLogger.error('测试错误', error);
|
||
});
|
||
```
|
||
|
||
### testReporter
|
||
|
||
提供测试报告生成器:
|
||
|
||
```typescript
|
||
test('使用测试报告', async ({ testReporter }) => {
|
||
testReporter.recordTestResult({
|
||
testName: '测试名称',
|
||
status: 'passed',
|
||
duration: 1000
|
||
});
|
||
});
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
### 1. 测试用例组织
|
||
|
||
- 使用 `test.describe` 组织相关的测试用例
|
||
- 使用 `test.beforeEach` 和 `test.afterEach` 设置测试前置和后置条件
|
||
- 为每个测试用例提供清晰的描述
|
||
|
||
```typescript
|
||
test.describe('用户管理功能', () => {
|
||
test.beforeEach(async ({ pageObjects, testData }) => {
|
||
await pageObjects.loginPage.navigate();
|
||
await pageObjects.loginPage.login(testData.admin.username, testData.admin.password);
|
||
});
|
||
|
||
test.afterEach(async ({ helpers }) => {
|
||
await helpers.screenshot.takeScreenshot('after-test');
|
||
});
|
||
|
||
test('创建用户', async ({ pageObjects }) => {
|
||
// 测试逻辑
|
||
});
|
||
});
|
||
```
|
||
|
||
### 2. 页面对象使用
|
||
|
||
- 始终使用页面对象而不是直接操作页面元素
|
||
- 将页面选择器封装在页面对象中
|
||
- 在页面对象中实现业务逻辑方法
|
||
|
||
```typescript
|
||
// 好的做法
|
||
await pageObjects.loginPage.login('admin', 'password');
|
||
|
||
// 不好的做法
|
||
await page.fill('input[placeholder="请输入用户名"]', 'admin');
|
||
await page.fill('input[placeholder="请输入密码"]', 'password');
|
||
await page.click('button[type="submit"]');
|
||
```
|
||
|
||
### 3. 测试数据管理
|
||
|
||
- 使用测试数据生成器创建测试数据
|
||
- 避免硬编码测试数据
|
||
- 使用测试夹具提供的预定义数据
|
||
|
||
```typescript
|
||
// 好的做法
|
||
const userData = testDataGenerator.generateUserData({
|
||
username: 'testuser',
|
||
status: 'active'
|
||
});
|
||
|
||
// 不好的做法
|
||
const userData = {
|
||
username: 'testuser',
|
||
password: 'password',
|
||
email: 'test@example.com',
|
||
// ... 硬编码的数据
|
||
};
|
||
```
|
||
|
||
### 4. 错误处理
|
||
|
||
- 在测试用例中使用 try-catch 捕获错误
|
||
- 使用测试日志记录错误信息
|
||
- 在测试失败时截图
|
||
|
||
```typescript
|
||
test('测试用例', async ({ testLogger, helpers }) => {
|
||
testLogger.startTest('测试用例');
|
||
|
||
try {
|
||
// 测试逻辑
|
||
testLogger.endTest('测试用例', 'passed');
|
||
} catch (error) {
|
||
testLogger.endTest('测试用例', 'failed', error as Error);
|
||
await helpers.screenshot.takeScreenshot('test-failure');
|
||
throw error;
|
||
}
|
||
});
|
||
```
|
||
|
||
### 5. 等待策略
|
||
|
||
- 使用页面对象提供的等待方法
|
||
- 避免使用固定的等待时间
|
||
- 使用 Playwright 的自动等待机制
|
||
|
||
```typescript
|
||
// 好的做法
|
||
await pageObjects.dashboardPage.waitForLoad();
|
||
await page.waitForSelector('.element', { state: 'visible' });
|
||
|
||
// 不好的做法
|
||
await page.waitForTimeout(5000);
|
||
```
|
||
|
||
### 6. 断言使用
|
||
|
||
- 使用 Playwright 的 expect 断言
|
||
- 提供有意义的断言消息
|
||
- 验证关键的业务逻辑
|
||
|
||
```typescript
|
||
// 好的做法
|
||
expect(pageTitle).toContain('仪表盘');
|
||
expect(successMessage).toBeTruthy();
|
||
expect(rowCount).toBeGreaterThan(0);
|
||
|
||
// 不好的做法
|
||
expect(pageTitle).toBeTruthy();
|
||
```
|
||
|
||
## 故障排查
|
||
|
||
### 测试失败时的调试
|
||
|
||
1. 查看测试日志:`test-results/logs/`
|
||
2. 查看截图:`test-results/screenshots/`
|
||
3. 查看测试报告:`test-results/reports/`
|
||
4. 使用调试模式运行:`npx playwright test --debug`
|
||
|
||
### 常见问题
|
||
|
||
1. **元素未找到**
|
||
- 检查选择器是否正确
|
||
- 确保元素已加载
|
||
- 使用适当的等待策略
|
||
|
||
2. **测试超时**
|
||
- 增加超时配置
|
||
- 检查网络请求是否正常
|
||
- 优化测试等待策略
|
||
|
||
3. **Mock服务不工作**
|
||
- 确认Mock服务已启用
|
||
- 检查Mock配置是否正确
|
||
- 验证Mock响应格式
|
||
|
||
## 扩展开发
|
||
|
||
### 添加新的页面对象
|
||
|
||
1. 在 `pages/` 目录下创建新的页面类
|
||
2. 继承 `BasePage` 类
|
||
3. 实现页面特定的方法和选择器
|
||
|
||
```typescript
|
||
import { BasePage } from './base-page';
|
||
|
||
export class NewPage extends BasePage {
|
||
private readonly selectors = {
|
||
// 页面选择器
|
||
};
|
||
|
||
constructor(page: Page) {
|
||
super(page);
|
||
}
|
||
|
||
// 页面方法
|
||
}
|
||
```
|
||
|
||
### 添加新的辅助工具
|
||
|
||
1. 在 `helpers/` 目录下创建新的辅助工具类
|
||
2. 实现辅助工具方法
|
||
3. 在测试夹具中注册新的辅助工具
|
||
|
||
```typescript
|
||
export class NewHelper {
|
||
private page: Page;
|
||
|
||
constructor(page: Page) {
|
||
this.page = page;
|
||
}
|
||
|
||
// 辅助工具方法
|
||
}
|
||
```
|
||
|
||
### 添加新的测试数据生成器
|
||
|
||
1. 在 `test-data.ts` 中添加新的数据生成方法
|
||
2. 定义数据接口
|
||
3. 实现数据生成逻辑
|
||
|
||
```typescript
|
||
export interface NewData {
|
||
// 数据接口
|
||
}
|
||
|
||
generateNewData(overrides: Partial<NewData> = {}): NewData {
|
||
// 数据生成逻辑
|
||
}
|
||
```
|
||
|
||
## 性能优化
|
||
|
||
### 并行执行
|
||
|
||
Playwright 默认支持并行执行测试,可以通过配置文件调整:
|
||
|
||
```typescript
|
||
export default defineConfig({
|
||
workers: 4, // 并发工作进程数
|
||
fullyParallel: true, // 完全并行执行
|
||
});
|
||
```
|
||
|
||
### 测试隔离
|
||
|
||
确保每个测试用例都是独立的,避免测试之间的依赖:
|
||
|
||
```typescript
|
||
test.beforeEach(async ({ page }) => {
|
||
// 清理测试数据
|
||
// 重置测试状态
|
||
});
|
||
```
|
||
|
||
### 重试机制
|
||
|
||
配置测试失败时的重试次数:
|
||
|
||
```typescript
|
||
export default defineConfig({
|
||
retries: 2, // 失败时重试次数
|
||
});
|
||
```
|
||
|
||
## 持续集成
|
||
|
||
### CI/CD集成
|
||
|
||
在CI/CD流程中集成E2E测试:
|
||
|
||
```yaml
|
||
# .github/workflows/e2e-tests.yml
|
||
name: E2E Tests
|
||
|
||
on: [push, pull_request]
|
||
|
||
jobs:
|
||
test:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v2
|
||
- uses: actions/setup-node@v2
|
||
- run: npm ci
|
||
- run: npm run test:e2e
|
||
- uses: actions/upload-artifact@v2
|
||
if: failure()
|
||
with:
|
||
name: test-results
|
||
path: test-results/
|
||
```
|
||
|
||
## 总结
|
||
|
||
本E2E测试工具提供了完整的端到端测试解决方案,包括:
|
||
|
||
- ✅ 模块化的测试用例编写
|
||
- ✅ 统一的测试环境配置
|
||
- ✅ 常用测试操作的封装与复用
|
||
- ✅ 清晰的测试报告与日志输出
|
||
- ✅ 页面对象模型(POM)
|
||
- ✅ 测试辅助工具
|
||
- ✅ Mock服务集成
|
||
- ✅ 测试数据生成
|
||
|
||
通过使用本工具,可以高效地编写、执行和维护E2E测试,确保应用的质量和稳定性。
|