Playwright 页面对象模型(POM)
本章介绍页面对象模型(Page Object Model)的设计思想和实现方式,帮助你组织大型测试套件。
什么是 POM
页面对象模型是一种测试设计模式,将页面的元素和操作封装到独立的类中。
每个页面对象代表 Web 应用中的一个页面或组件,包含该页面的定位器和操作方法。
POM 的优势
| 优势 | 说明 |
|---|---|
| 简化测试编写 | 测试代码使用高层 API(如 loginPage.login('user', 'pass'))而非原始的 Locator 操作 |
| 降低维护成本 | 页面结构的变更只需要修改 Page Object 类,不需要修改每个测试 |
| 提高可读性 | 测试代码更像业务语言,非技术人员也能理解 |
| 避免重复 | 相同的定位器和操作只需定义一次 |
创建 Page Object 类
下面以 RUNOOB 网站为例,创建一个登录页面的 Page Object。
实例
// 文件路径:pages/LoginPage.ts
import { type Page, type Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
// 将所有定位器声明为只读属性
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
// 在构造函数中初始化所有定位器
this.usernameInput = page.getByLabel('用户名');
this.passwordInput = page.getByLabel('密码');
this.loginButton = page.getByRole('button', { name: '登录' });
this.errorMessage = page.getByTestId('login-error');
}
// 导航到登录页
async goto() {
await this.page.goto('/login');
}
// 执行登录操作
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
// 验证登录失败的错误提示
async expectLoginError(message: string) {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toContainText(message);
}
}
import { type Page, type Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
// 将所有定位器声明为只读属性
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
// 在构造函数中初始化所有定位器
this.usernameInput = page.getByLabel('用户名');
this.passwordInput = page.getByLabel('密码');
this.loginButton = page.getByRole('button', { name: '登录' });
this.errorMessage = page.getByTestId('login-error');
}
// 导航到登录页
async goto() {
await this.page.goto('/login');
}
// 执行登录操作
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
// 验证登录失败的错误提示
async expectLoginError(message: string) {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toContainText(message);
}
}
在测试中使用 Page Object
实例
// 文件路径:tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('登录功能', () => {
test('正确用户名密码登录成功', async ({ page }) => {
const loginPage = new LoginPage(page);
// 使用 Page Object 的方法
await loginPage.goto();
await loginPage.login('runoob_user', 'correct_password');
// 登录成功后验证跳转
await expect(page).toHaveURL('/dashboard');
});
test('错误密码登录失败', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('runoob_user', 'wrong_password');
// 使用 Page Object 的断言方法
await loginPage.expectLoginError('用户名或密码错误');
});
});
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('登录功能', () => {
test('正确用户名密码登录成功', async ({ page }) => {
const loginPage = new LoginPage(page);
// 使用 Page Object 的方法
await loginPage.goto();
await loginPage.login('runoob_user', 'correct_password');
// 登录成功后验证跳转
await expect(page).toHaveURL('/dashboard');
});
test('错误密码登录失败', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('runoob_user', 'wrong_password');
// 使用 Page Object 的断言方法
await loginPage.expectLoginError('用户名或密码错误');
});
});
多层页面对象
在真实项目中,可以创建多个 Page Object,它们之间可以相互关联。
实例
// 文件路径:pages/DashboardPage.ts
import { type Page, type Locator, expect } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
readonly welcomeText: Locator;
readonly logoutButton: Locator;
constructor(page: Page) {
this.page = page;
this.welcomeText = page.getByText('欢迎回来');
this.logoutButton = page.getByRole('button', { name: '退出' });
}
async expectWelcomeMessage(username: string) {
await expect(this.welcomeText).toContainText(username);
}
async logout() {
await this.logoutButton.click();
}
}
import { type Page, type Locator, expect } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
readonly welcomeText: Locator;
readonly logoutButton: Locator;
constructor(page: Page) {
this.page = page;
this.welcomeText = page.getByText('欢迎回来');
this.logoutButton = page.getByRole('button', { name: '退出' });
}
async expectWelcomeMessage(username: string) {
await expect(this.welcomeText).toContainText(username);
}
async logout() {
await this.logoutButton.click();
}
}
页面对象的组合使用:
实例
test('完整的登录登出流程', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
// 登录
await loginPage.goto();
await loginPage.login('runoob_user', 'password');
// 验证仪表盘
await dashboardPage.expectWelcomeMessage('runoob_user');
// 登出
await dashboardPage.logout();
await expect(page).toHaveURL('/login');
});
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
// 登录
await loginPage.goto();
await loginPage.login('runoob_user', 'password');
// 验证仪表盘
await dashboardPage.expectWelcomeMessage('runoob_user');
// 登出
await dashboardPage.logout();
await expect(page).toHaveURL('/login');
});
POM 最佳实践
| 实践 | 说明 |
|---|---|
| Locator 集中在构造函数 | 所有定位器在构造函数中定义,不要在方法中动态创建 |
| 方法返回 Page Object | 操作方法可以返回新的 Page Object(如登录后返回 DashboardPage) |
| 断言封装但不滥用 | 常用的断言组合封装为方法,简单的断言直接在测试中写 |
| 不用继承用组合 | 避免深层继承,使用组合模式关联多个 Page Object |
| 一套对象覆盖一个页面/组件 | 粒度适中,不要为每个小元素创建单独的类 |
POM 不是强制要求。
对于只有几个测试的小项目,直接在测试中写 Locator 也更清晰。当测试文件超过 5 个以上时,考虑引入 POM。
