在编写 Cypress e2e 测试时,几乎每个测试用例都需要:
这导致每个测试的 beforeEach
都包含大量重复代码(17+ 行),不仅冗长,而且难以维护。
示例(重复的代码):
beforeEach(() => {
cy.clearAllSessionStorage();
cy.clearAllLocalStorage();
mockAllRequiredAPIs(); // 软件信息、日志
mockI18nSuccess('zh'); // 多语言资源
mockGetQuotaSuccess(); // 配额检查
mockGetPatientTypeHuman(); // 患者类型 - 必需!
mockGetBodyPartHuman(); // 身体部位 - 必需!
mockLoginSuccess(); // 登录
loginPage.visit();
loginPage.login('admin', '123456');
cy.wait('@loginSuccess');
cy.contains('登录成功').should('be.visible');
});
创建自定义 Cypress 命令 cy.loginAndInitialize()
,将所有登录初始化逻辑封装成一行代码。
使用 Cypress 自定义命令(Custom Commands)机制,通过 Cypress.Commands.add()
注册全局命令。
在 cypress/support/commands.js
中添加:
Cypress.Commands.add(
'loginAndInitialize',
(username = 'admin', password = '123456') => {
// 导入所需的 mock handlers
const {
mockAllRequiredAPIs,
mockI18nSuccess,
} = require('./mock/handlers/i18n');
const { mockGetQuotaSuccess } = require('./mock/handlers/quota');
const {
mockGetPatientTypeHuman,
mockGetBodyPartHuman,
} = require('./mock/handlers/protocol');
const { mockLoginSuccess } = require('./mock/handlers/user');
const LoginPage = require('./pageObjects/LoginPage').default;
// 清除存储
cy.clearAllSessionStorage();
cy.clearAllLocalStorage();
// Mock 所有必要的 API
mockAllRequiredAPIs(); // 软件信息、日志
mockI18nSuccess('zh'); // 多语言资源
mockGetQuotaSuccess(); // 配额检查
mockGetPatientTypeHuman(); // 患者类型 - 必需!
mockGetBodyPartHuman(); // 身体部位 - 必需!
mockLoginSuccess(); // 登录
// 使用 LoginPage Page Object 进行登录
const loginPage = new LoginPage();
loginPage.visit();
loginPage.login(username, password);
// 等待登录成功消息
cy.wait('@loginSuccess');
cy.contains('登录成功', { timeout: 5000 }).should('be.visible');
// 等待初始化完成 - 退出按钮可见表示已成功进入主页
cy.get('[data-testid="exit-button"]', { timeout: 10000 }).should(
'be.visible'
);
cy.log('✅ 登录并初始化完成');
}
);
创建 cypress/support/commands.d.ts
:
/// <reference types="cypress" />
declare namespace Cypress {
interface Chainable<Subject = any> {
/**
* 自定义命令:登录并完成初始化进入主页
*
* 这个命令封装了完整的登录流程:
* 1. 清除存储
* 2. Mock 所有必要的 API(i18n、quota、patient types、body parts)
* 3. 执行登录
* 4. 等待初始化完成
*
* @param username - 用户名,默认 'admin'
* @param password - 密码,默认 '123456'
*
* @example
* cy.loginAndInitialize();
* cy.loginAndInitialize('testuser', 'testpass');
*/
loginAndInitialize(
username?: string,
password?: string
): Chainable<Subject>;
}
}
export {};
之前(17 行):
import {
mockLoginSuccess,
mockLogoutSuccess,
} from '../../support/mock/handlers/user';
import {
mockI18nSuccess,
mockAllRequiredAPIs,
} from '../../support/mock/handlers/i18n';
import { mockGetQuotaSuccess } from '../../support/mock/handlers/quota';
import {
mockGetPatientTypeHuman,
mockGetBodyPartHuman,
} from '../../support/mock/handlers/protocol';
import LoginPage from '../../support/pageObjects/LoginPage';
describe('User Logout', () => {
const loginPage = new LoginPage();
beforeEach(() => {
cy.clearAllSessionStorage();
cy.clearAllLocalStorage();
mockAllRequiredAPIs();
mockI18nSuccess('zh');
mockGetQuotaSuccess();
mockGetPatientTypeHuman();
mockGetBodyPartHuman();
mockLoginSuccess();
loginPage.visit();
loginPage.login('admin', '123456');
cy.wait('@loginSuccess');
cy.contains('登录成功').should('be.visible');
});
it('should logout successfully', () => {
// 测试代码
});
});
之后(1 行):
import { mockLogoutSuccess } from '../../support/mock/handlers/user';
import LoginPage from '../../support/pageObjects/LoginPage';
describe('User Logout', () => {
const loginPage = new LoginPage();
beforeEach(() => {
cy.loginAndInitialize();
});
it('should logout successfully', () => {
// 测试代码
});
});
describe('Main Page Tests', () => {
beforeEach(() => {
// 使用默认用户名 'admin' 和密码 '123456'
cy.loginAndInitialize();
});
it('should display main page correctly', () => {
// 现在已经在主页了,可以直接开始测试
cy.get('[data-testid="exit-button"]').should('be.visible');
});
});
describe('Different User Tests', () => {
beforeEach(() => {
// 使用自定义用户名和密码
cy.loginAndInitialize('testuser', 'testpass123');
});
it('should work with different user', () => {
// 测试代码
});
});
describe('Specific Test', () => {
it('should handle logout correctly', () => {
// 在测试内部登录
cy.loginAndInitialize();
// 执行测试操作
cy.get('[data-testid="exit-button"]').click();
cy.get('[data-testid="exit-modal-logout-button"]').click();
// 验证返回登录页
cy.get('[data-testid="login-username-input"]').should('be.visible');
});
});
import MainPage from '../../support/pageObjects/MainPage';
import ExitModalPage from '../../support/pageObjects/ExitModalPage';
describe('Logout Flow', () => {
const mainPage = new MainPage();
const exitModalPage = new ExitModalPage();
beforeEach(() => {
cy.loginAndInitialize();
});
it('should successfully logout', () => {
// 使用 Page Object 执行操作
mainPage.clickExitButton();
exitModalPage.shouldBeVisible();
exitModalPage.clickLogout();
// 验证登出成功
cy.contains('已退出登录').should('be.visible');
});
});
describe('Multiple Login Tests', () => {
it('should support multiple logins in same test', () => {
// 第一次登录
cy.loginAndInitialize('user1', 'pass1');
cy.get('[data-testid="exit-button"]').should('be.visible');
// 登出
cy.get('[data-testid="exit-button"]').click();
cy.get('[data-testid="exit-modal-logout-button"]').click();
// 第二次登录
cy.loginAndInitialize('user2', 'pass2');
cy.get('[data-testid="exit-button"]').should('be.visible');
});
});
cy.loginAndInitialize()
命令封装了以下操作:
cy.clearAllSessionStorage();
cy.clearAllLocalStorage();
Mock 所有必要的 API
mockAllRequiredAPIs()
- 软件信息、日志 APImockI18nSuccess('zh')
- 多语言资源mockGetQuotaSuccess()
- 配额检查mockGetPatientTypeHuman()
- 患者类型(AppInitializer 必需)mockGetBodyPartHuman()
- 身体部位(AppInitializer 必需)mockLoginSuccess()
- 登录 API执行登录
loginPage.visit();
loginPage.login(username, password);
cy.wait('@loginSuccess');
cy.contains('登录成功', { timeout: 5000 }).should('be.visible');
等待初始化完成
cy.get('[data-testid="exit-button"]', { timeout: 10000 }).should(
'be.visible'
);
TypeScript 类型定义提供:
应用的 AppInitializer
组件在登录后会执行初始化操作:
// AppInitializer 初始化流程
1. getPatientTypes() // 获取患者类型列表
2. getBodyParts() // 获取身体部位列表
3. initializeProductState() // 初始化产品状态
只有这三个操作全部完成后,才会调用 onInitialized()
进入主界面。
如果缺少相应的 Mock:
这些超时值经过测试验证,足够应对正常的初始化流程。
// ✅ 正确
beforeEach(() => {
cy.loginAndInitialize();
});
// ❌ 错误 - 不要在 describe 块外调用
cy.loginAndInitialize();
describe('Tests', () => {});
确保 Mock Handlers 已正确导入
Page Objects 的使用
LoginPage
Page ObjectLoginPage
实现正确环境变量
只需修改 cypress/support/commands.js
中的 loginAndInitialize
命令,所有使用该命令的测试会自动更新。
示例:添加新的 Mock
Cypress.Commands.add(
'loginAndInitialize',
(username = 'admin', password = '123456') => {
// ... 现有代码 ...
// 添加新的 Mock
mockNewRequiredAPI();
// ... 现有代码 ...
}
);
可以创建变体命令:
// 快速登录(跳过某些等待)
Cypress.Commands.add('loginQuick', (username, password) => {
// 简化的登录流程
});
// 登录到特定页面
Cypress.Commands.add('loginToPage', (username, password, pagePath) => {
cy.loginAndInitialize(username, password);
cy.visit(pagePath);
});
方面 | 封装前 | 封装后 |
---|---|---|
代码行数 | 17+ 行 | 1 行 |
导入依赖 | 5+ 个 import | 不需要 |
可维护性 | 低(每个测试都要改) | 高(只改一处) |
易用性 | 复杂 | 简单 |
类型安全 | 无 | 有(TypeScript) |
错误风险 | 高(容易遗漏 Mock) | 低(一次性配置) |
对于特殊场景,可以直接手动编写登录逻辑,或创建专门的变体命令。