点击蓝字
关注我们
页面对象模型(POM)给自动化测试代码搭建了一套清晰好用的设计规范。
如果你有一点写代码的经验、正在搭建自动化测试框架,或是被一堆杂乱难读的测试脚本折腾了很久,想找一套更省心的落地方案,那 POM 刚好适配你的需求,这篇文章会完整带你吃透这套思路。
什么是 POM?
POM 是适配自动化测试架构、清爽简洁的代码设计模式,一句话就能分清两类代码的分工:测试脚本负责做校验,页面对象负责执行页面操作。
说得更细致一点:测试脚本把控整体业务流程、校验应用运行状态;页面对象负责执行页面各类操作、反馈页面实时状态。
●测试脚本下达指令:“选中这个按钮”
●页面对象执行按钮选中操作
●接着测试脚本发起校验:“弹窗有没有弹出来?”
●页面对象负责获取弹窗元素
它还有一个很实用的优势:所有测试脚本共用一套可复用的页面类库。
把元素定位器、页面操作逻辑全部收拢到公共类文件之后,测试代码会更好维护,页面微调也不会大面积跑崩。
你还会发现大量高频复用逻辑,可以抽成工具类,或是直接放进全局页面类中。
上面这张架构图完整展示了 POM 四大模块各自的职责:
测试类(Test Class)
●把控业务流程走向
●管理页面运行状态
●编写各类断言校验逻辑
全局页面类(Global Page Class)
●存放全局页面对象实例
●处理 iframe 嵌套页面
●封装全局通用工具、超时等待方法
页面类(Page Class)
●执行各类页面交互动作
●统一存放所有元素定位器
●处理页面特殊交互场景
工具辅助类(Helper Class)
●封装可复用通用工具函数
●处理各类网络请求
●绑定浏览器事件监听
POM 的由来
POM 是自动化测试框架领域成熟通用的设计模式与行业标准最佳实践,诞生的初衷就是解决大型自动化测试套件难以扩容、维护成本居高不下的痛点。
早些年写自动化脚本都是逐条单独开发,几乎没有配套框架和分层架构。每条用例内部都硬编码写死元素定位、自定义操作逻辑、超时等待代码,写法和 Playwright 官方示例完全一致:
test('get started link', async ({ page }) => {await page.goto('https://playwright.dev/');// Click the get started link.await page.getByRole('link', { name: 'Get started' }).click();// Expects page to have a heading with the name of Installation.await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();});
这种写法里,每条脚本都要自己维护专属定位器和操作逻辑,项目规模一扩大根本扛不住。举个很直观的例子:
几十条用例都需要点击「Get started」链接,如果后续产品迭代把这段文案改成「Click Here!」,几十条用例会全部执行失败,你只能逐个打开脚本修改定位文本。
这会带来毁灭性的维护负担:每条测试用例几乎等同于一套独立代码工程,里面所有自定义交互逻辑都要单独调整,前端页面随便改一处内容,就要批量修改几十条测试脚本。
之后整洁代码架构、GRASP 通用职责分配设计原则慢慢渗透到自动化测试领域,行业沉淀出三条核心开发准则:
●DRY(不要重复造轮子):把固定操作封装成独立函数,全量测试用例直接复用
●SOLID 设计原则:按页面维度拆分独立类,类内存放该页面全部交互逻辑;严格划分权责,测试脚本只做断言、页面对象只做页面操作
●KISS(保持简单易懂):制定高可读性代码标准,每个函数的功能一眼就能看懂
遵循这套规范之后,产品页面逻辑发生变更时,我们只需要修改对应页面类一处代码,不用改动成千上万条测试用例。
自动化套件可以平稳支撑超大型系统迭代,脚本稳定性大幅提升,逻辑修改全部集中处理,新增功能也能基于现有代码扩展,不用重复堆砌冗余代码。
这套适配自动化测试场景的规范合集,就是我们所说的页面对象模型(POM)。它依托面向对象设计思路,用名为「页面」的各类类文件对被测系统做建模。
我们把上面那段原生示例改写成 POM 写法,差异一眼就能看清:只需要新建统一的HomePage类,收拢全部定位器和页面操作。后续前端页面改版,仅需修改HomePage这一个文件即可。
test('get started link', async ({ page }) => {const home_page = new HomePage(page);await home_page.goto();// Click the get started link.await home_page.clickGetStarted();// Expects page to have a heading with the name of Installation.await expect(home_page.header).toBeVisible();});
一套更合理的落地思路
Playwright 官方标注的最佳实践,完全没有适配 POM 分层思路,甚至里面不少写法都属于POM 反模式。
●Playwright 自带自动重试断言能力,官方推荐直接在测试脚本内部定义元素定位器
●官方给出的定位写法,经常把断言逻辑和元素定位代码混杂在一起
●官方建议网络请求、浏览器事件直接写在测试用例内部,还要求在触发页面操作前提前注册捕获监听函数
撰写本文时,Playwright 文档虽然提供了一段 POM 示例,但不仅内容写得十分简略,示例代码本身还存在多处不符合 POM 规范的问题:
●在测试用例内部调用page.locator,将页面业务逻辑混入测试脚本
●在函数内部修改定位器(规范要求定位器必须静态定义)
●在构造函数内实例化定位器(定位器应当直接写在类顶层,才能完整触发代码智能提示)
下面我们来搭建一套分层更规范的实操示例。
Playwright 本身是一款非常优秀的现代化自动化工具,而 POM 依旧能解决绝大多数框架维护难题。
我们完全可以把两者优势结合,兼顾工具原生能力与分层架构,大幅降低长期维护成本。
拆分Playwright官网页面(划分页面类)
本次示例以 Playwright 官网作为被测站点,行业内没有硬性标准界定什么是独立「页面」,划分的核心准则只有两点:封装性、可复用性。
按照这个标准,我把 Playwright 官网首页拆分成 3 个组件用于演示:
●home_page.ts:存放首页全部专属页面内容与交互逻辑
●header_page.ts:导航栏全站通用,首页类可以引用该文件,复用导航栏所有操作方法
●search_dialog_page.ts:和导航栏一致,搜索弹窗全站通用,会弹出独立浮层,和主页面解耦
当然你也可以拆分底部栏 footer_page.ts 等其他页面组件,但本次演示用这三类就足够清晰。
上面的截图展示了 pages 文件夹目录,能看到 header_page、home_page、search_dialog_page 三个页面类文件。
搭建通用页面基类
我们先封装一套规范的页面对象构造器。老旧自动化框架会直接扩展、覆写全局页面类,但 Playwright 自带的全局 Page 对象,不建议随意修改、覆盖。
我们换一种实现方案:自定义一个通用页面基类,所有业务页面类都继承这个基类。
这个基类会完成三件核心工作:
●封装 Playwright 原生全局 Page 实例,让所有子页面都能调用页面内置全部方法
●优化定位器的智能提示、代码重构跳转体验
●存放各类全局通用工具函数,很多子页面都会用到,比如全局统一等待超时方法
// model_page.ts import { FrameLocator, Page } from "@playwright/test";exportclassModel_Page {readonly page: Page;readonly iFrame: FrameLocator | Page;constructor(page: Page, iFrame?: FrameLocator | Page) {this.page = page;this.iFrame = iFrame;if (!iFrame) {this.iFrame = page;}}}
编写带静态定位器的业务页面类
接下来我们编写业务页面类。所有页面类都要继承Model_Page通用页面基类,并且必须在构造函数中调用super()完成初始化。
因为基类实例已经提前创建完成,TypeScript 支持我们直接在类顶层定义定位器常量,不用在构造函数里批量赋值只读变量。这么写有两个直观好处:
●鼠标悬浮对应变量时,会直接展示完整定位器表达式
●按下 F12 可以一键跳转到定位器定义位置,不用在冗长的构造函数里翻找代码
我们的首页页面类写法很简洁,包含直接跳转对应网址的逻辑,以及「Get started」按钮的定位器。
导航栏全站通用,我们可以直接在首页类中实例化导航页面对象,方便后续调用。
// home_page.ts import { Model_Page } from "@pages/model_page";import { FrameLocator, Page } from "@playwright/test";import { Header_Page } from "./header_page";exportclassHome_PageextendsModel_Page {readonly HEADER: Header_Page;readonly GET_STARTED_BUTTON = this.page.getByRole("link", {name: "Get started",});constructor(page: Page, iFrame?: FrameLocator | Page) {super(page, iFrame);this.HEADER = new Header_Page(this.page, this.iFrame);}async gotoHome(): Promise<Home_Page> {await this.page.goto("/");returnthis;}}
依托这个页面类,我们就能写出极简的基础测试用例,完成页面打开、按钮链接地址校验:
// home_basic_tests.spec.ts import { Home_Page } from "@pages/playwright-dev/home_page";import { expect, test } from "@playwright/test";test("Basic Home Page Test", { tag: ["@e2e", "@home"] }, async ({ page }) => {const home_page = new Home_Page(page);await home_page.gotoHome();expect(home_page.GET_STARTED_BUTTON).toHaveAttribute("href", "/docs/intro");});
未完待续
下期我们继续学习封装交互操作、处理路由网络请求、浏览器事件等。

