一、项目介绍
在前后端分离成为主流的今天,Web UI 自动化测试是保障前端质量、回归验证的核心手段。传统 Selenium 框架存在元素等待繁琐、驱动版本匹配困难、多场景调试能力弱等痛点,而微软开源的 Playwright 凭借自动等待、多引擎原生支持、强大的追踪能力,逐渐成为 Web 自动化的首选方案。
本文将基于一套完整的企业级项目实践,详细拆解Playwright + Pytest + Allure + POM的 UI 自动化测试框架设计,从目录架构、核心分层、Fixture 设计到 Mock 机制与报告体系,全方位讲解可落地的自动化测试工程化方案。
技术栈:Python+pytest+playwright+Ajax+allure+Jenkins+docker+Linux
gitee:https://gitee.com/xiaomalou/playwright-ui.git
二、项目整体架构
D:\playwright-ui ├── .venv/ # 虚拟环境(Python 和所有第三方库都装这里) ├── .idea/ # PyCharm 的项目配置,不用管 ├── pages/ # ★ 页面对象层:每个页面封装成一个类 │ ├── login_page.py # 登录页 │ ├── register_page.py # 注册页 │ ├── project_list_page.py # 项目列表页 │ ├── add_project_page.py # 新增项目页 │ ├── add_module_page.py # 新增模块页 │ └── list_env_page.py # 环境列表页 ├── cases/ # ★ 测试用例层:写测试的地方 │ ├── conftest.py # 用例层的 fixture(全局登录、独立登录上下文) │ ├── test_login.py # 登录用例 │ ├── test_register.py # 注册用例 │ ├── test_project_list.py # 项目列表用例 │ ├── test_add_project.py # 新增项目用例 │ ├── test_add_module.py # 新增模块用例 │ ├── test_env_list.py # 环境列表用例 │ └── more_accounts/ # 多账号切换演示 │ ├── conftest.py # admin 管理员上下文 │ └── test_x.py # A 账号创建、B 账号删除 ├── mocks/ # ★ Mock 数据:拦截接口返回假数据 │ └── mock_api.py ├── plugins/ # ★ 二次开发插件(官方插件源码的本地改造版) │ ├── pytest_playwright.py # 复制官方插件 + 增加 allure 截图/视频 │ └── pytest_base_url_plugin.py # 复制官方 base-url 插件 ├── conftest.py # pytest 全局配置(根目录) ├── pytest.ini # pytest 运行规则配置 ├── run.py # 框架运行入口 ├── requirements.txt # 依赖清单 ├── reports/ # allure 结果文件(运行自动生成) ├── allure_report/ # 生成的 HTML 测试报告(运行自动生成) ├── test-results/ # 截图、视频、trace 临时文件(运行自动生成) └── README.md # 项目说明- pages 层:页面对象模型的核心,每个页面对应一个类,内部封装元素定位器、页面操作方法,测试用例不直接接触元素定位,只调用页面方法。
- cases 层:业务测试用例,按模块拆分,通过 Fixture 注入页面对象,聚焦业务场景与断言逻辑。
- mocks 层:统一管理接口 Mock 数据,覆盖成功、失败、空数据、服务器异常等场景,实现前端 UI 的全场景验证。
- plugins 层:对官方 Playwright 插件与 base_url 插件进行二次开发,扩展窗口最大化、HTTPS 忽略、全局启动参数等定制化能力。
- conftest 体系:分层设计 Fixture,全局层管理浏览器 / 上下文,用例层管理单页面生命周期与失败捕获。
三、核心架构详细解
1. POM 页面对象模型:实现元素与用例解耦
框架严格遵循 POM 设计原则,将每个页面的元素定位、操作逻辑封装为独立的 Page 类,测试用例仅关注业务流程与结果断言。
设计示例:pages/login_page.py
from playwright.sync_api import Page class LoginPage: def __init__(self, page: Page): self.page = page self.locator_username = page.get_by_label("用 户 名:") self.locator_password = page.get_by_label("密 码:") self.locator_login_btn = page.locator('text=立即登录') self.locator_register_link = page.locator('text=没有账号?点这注册') # 用户名输入框提示语 self.locator_username_tip1 = page.locator('[data-fv-validator="notEmpty"][data-fv-for="username"]') self.locator_username_tip2 = page.locator('[data-fv-validator="stringLength"][data-fv-for="username"]') self.locator_username_tip3 = page.locator('[data-fv-validator="regexp"][data-fv-for="username"]') # 密码输入框提示语 self.locator_password_tip1 = page.locator('[data-fv-validator="notEmpty"][data-fv-for="password"]') self.locator_password_tip2 = page.locator('[data-fv-validator="stringLength"][data-fv-for="password"]') self.locator_password_tip3 = page.locator('[data-fv-validator="regexp"][data-fv-for="password"]') # 账号或密码不正确! self.locator_login_error = page.locator('text=账号或密码不正确!') def navigate(self): self.page.goto("/login.html") def fill_username(self, username): self.locator_username.fill(username) def fill_password(self, password): self.locator_password.fill(password) def click_login_button(self): self.locator_login_btn.click() def click_register_link(self): self.locator_register_link.click() def login(self, username, password) -> None: """完整登录操作""" self.locator_username.fill(username) self.locator_password.fill(password) self.locator_login_btn.click()- 元素定位仅维护一份,页面迭代时只需修改 Page 类,无需改动用例
- 操作方法语义化,测试用例可读性大幅提升
- 支持 Allure 步骤埋点,报告中可清晰看到每一步操作
2. Fixture 分层设计:上下文隔离与生命周期管理
框架基于 Pytest Fixture ,同时解决了「登录态复用」与「非登录场景隔离」的核心矛盾。
(1)全局浏览器与上下文(session 级)
在plugins/pytest_playwright.py中定义 session 级别的browser和contextFixture,全局只启动一次浏览器,复用浏览器进程,提升执行效率。同时内置失败自动截图、录屏、Trace 追踪能力。
(2)登录态全局复用
针对需登录的业务场景,提供login_firstFixture(session 级别),执行前登录一次并保存上下文状态,后续所有登录态用例直接复用,避免每个用例重复登录。
@pytest.fixture(scope="session") def login_first(context, base_url, pytestconfig) -> None: """有些网站网页关闭cookie就失效了,全局登录一次""" # context = browser.new_context(base_url=base_url, no_viewport=True) print("base_url----", base_url) page = context.new_page() LoginPage(page).navigate() LoginPage(page).login("py", "123456") # 等待登录成功页面重定向 page.wait_for_url(url='**/index.html')(3)非登录场景独立上下文
针对登录、注册等无需前置登录的页面,提供unlogin_contextFixture(module 级别),创建独立的浏览器上下文,完全隔离 Cookie,避免全局登录态导致登录页直接跳转到首页的问题。
unlogin_page:为什么登录/注册要"另起炉灶"?
如果登录页用例也复用已登录的 context,浏览器会带着 cookie 直接跳到首页index.html,根本停不到登录页——用例就没法测了。所以unlogin_context用browser 新开一个干净 context(module 级),unlogin_page从它开页,不加载 cookie。代码里注释写得很直白:"避免全局先登录加载 cookie,导致有些打开登录页直接跳到首页去了"。
@pytest.fixture(scope="module") def unlogin_context(browser, base_url, pytestconfig, browser_context_args: Dict): """ 登录注册页面(不依赖于先登录)单独创建独立的 context 上下文 避免全局先登录加载cookie,导致有些打开登录页直接跳到首页去了 :return: """ context = browser.new_context(**browser_context_args) yield context context.close() @pytest.fixture def unlogin_page(unlogin_context: BrowserContext, pytestconfig: Any, request: pytest.FixtureRequest): """ 登录注册页面(不依赖于先登录)单独创建独立的 page 对象 带上用例失败截图和添加视频功能 """ pages: List[Page] = [] unlogin_context.on("page", lambda page: pages.append(page)) page = unlogin_context.new_page() yield page failed = request.node.rep_call.failed if hasattr(request.node, "rep_call") else True # 截图判断 screenshot_option = pytestconfig.getoption("--screenshot") capture_screenshot = screenshot_option == "on" or (failed and screenshot_option == "only-on-failure") print(f"capture_screenshot:{capture_screenshot}") if capture_screenshot: for index, page in enumerate(pages): human_readable_status = "failed" if failed else "finished" screenshot_path = _build_artifact_test_folder( pytestconfig, request, f"test-{human_readable_status}-{index + 1}.png" ) print(f'-----------------{screenshot_path}') try: page.screenshot(timeout=5000, path=screenshot_path) # 把截图放入allure报告 allure.attach.file(screenshot_path, name=f"{request.node.name}-{human_readable_status}-{index + 1}", attachment_type=allure.attachment_type.PNG ) except Error: pass page.close() # 用例添加视频 video_option = pytestconfig.getoption("--video") preserve_video = video_option == "on" or (failed and video_option == "retain-on-failure") if preserve_video: for page in pages: video = page.video if not video: continue try: video_path = video.path() file_name = os.path.basename(video_path) file_path = _build_artifact_test_folder(pytestconfig, request, file_name) video.save_as(path=file_path) # 放入视频 allure.attach.file(file_path, name=f"{request.node.name}-{human_readable_status}-{index + 1}", attachment_type=allure.attachment_type.WEBM) except Error: # Silent catch empty videos. pass(4)断言的几种姿势
① 页面元素断言:
expect(locator).to_be_visible() # 元素可见
expect(locator).to_contain_text("不能为空") # 包含某段文字
expect(locator).not_to_be_enabled() # 按钮不可点击(提交按钮 disabled)
expect(locator).to_have_value('test') # 输入框的值
expect(locator).to_have_count(1) # 匹配到 1 个元素
② 页面跳转断言:
expect(self.login.page).to_have_title("首页") # 断言标题
expect(self.login.page).to_have_url("/index.html") # 断言 URL
③ 显式等待导航:
with self.login.page.expect_navigation(url='**/index.html'):
self.login.click_login_button() # 点击后页面跳转,expect_navigation 等待跳转完成
④ 断言 Ajax 请求:
with self.login.page.expect_request('**/api/login') as req:
self.login.click_login_button() # 点登录按钮
assert req.value.method == 'POST' # 断言请求方法是 POST
assert req.value.header_value('content-type') == 'application/json' # 断言请求头
assert req.value.post_data_json == {'username': 'py', 'password': '123456'} # 断言请求体
⑤ 断言 Ajax 响应:
ith self.login.page.expect_response('**/api/login') as res:
self.login.click_login_button()
assert res.value.ok
assert res.value.status == 200
3. 接口 Mock 机制:前端 UI 全场景验证
通过 Playwright 原生的page.route()接口拦截能力,结合 mocks 层统一管理的模拟数据,可在不依赖后端的前提下,覆盖各类异常边界场景。
Mock 示例:模拟项目名称重复 400 场景
mock_project_400 = { "url": "**/api/project", "handler": lambda route: route.fulfill( status=400, body=json.dumps({ "errors": {"project_name": "yo yo 已存在"}, "message": "Input payload validation failed" }) ) }用例中使用 Mock
def test_add_project_400(self, page: Page): """项目名称重复,弹出模态框""" self.add_project.fill_project_name("yo yo") self.add_project.fill_publish_app("xx") # 拦截接口,返回400模拟数据 page.route(**mock_api.mock_project_400) self.add_project.click_save_button() # 断言前端提示 expect(self.add_project.locator_boot_box).to_be_visible() expect(self.add_project.locator_boot_box).to_contain_text('已存在')Mock 覆盖场景:
- 正常 200 返回、参数校验 400、服务器异常 500
- 列表空数据、单条数据、分页场景
- 权限不足 403、接口超时等异常场景
4. Allure 报告深度集成
框架实现了 Allure 报告的全方位集成,提升报告可读性与问题定位效率。
- 动态用例标题:自动读取用例 docstring 作为 Allure 用例标题,无需手动打标签
def pytest_runtest_call(item: Item): if item.parent._obj.__doc__: allure.dynamic.feature(item.parent._obj.__doc__) # 类 docstring → feature if item.function.__doc__: allure.dynamic.title(item.function.__doc__) # 方法 docstring → title - 失败附件自动嵌入:失败截图、录制视频、Trace 文件自动挂载到对应用例
allure.attach.file(screenshot_path, name=f"{request.node.name}-failed-1", attachment_type=allure.attachment_type.PNG) - 操作步骤埋点:页面操作方法通过
allure.step封装,报告中可还原完整操作路径@allure.step("导航到注册页") # 或用 f-string 带参数 def navigate(self): self.page.goto("/register.html") @allure.step(f"输入用户名:{username}") def fill_username(self, username): self.locator_username.fill(username)
5. 插件化扩展能力
项目未直接使用官方pytest-playwright插件,而是通过自定义插件实现了更贴合企业场景的扩展:
- 禁用官方插件,自主管理浏览器启动参数,默认窗口最大化、忽略 HTTPS 证书错误
addopts = -p no:playwright -p no:base_url ... - 统一 base_url 管理,支持命令行、ini 配置、环境变量多种配置方式
- 保留官方全部命令行参数(--headed、--browser、--tracing 等),同时新增定制化能力
四、框架运行与使用指南
1. 环境搭建
# 1. 安装Python依赖 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt # 2. 安装Playwright浏览器驱动 playwright install # 也可仅安装指定引擎 playwright install chromium2. 执行测试用例
方式一:运行入口文件
python run.py方式二:命令行执行
# 执行全部用例,生成Allure结果 pytest --alluredir ./reports # 指定模块执行 pytest cases/test_login.py --alluredir ./reports3. 查看测试报告
# 执行全部用例,生成Allure结果 pytest --alluredir ./reports # 指定模块执行 pytest cases/test_login.py --alluredir ./reports4. 失败问题追踪
框架默认开启「失败保留 Trace、截图、视频」策略,定位问题时可通过 Trace 文件完整复现执行过程:
# 命令行查看Trace playwright show-trace test-results/xxx/trace.zip # 网页端查看:访问 https://trace.playwright.dev/ 上传trace.zip五、框架优势与亮点
- 高稳定性:Playwright 原生自动等待机制,消除强制等待,解决元素未加载导致的用例不稳定问题
- 强可维护性:POM 分层设计,页面变更不影响用例逻辑,新增业务模块成本低
- 调试效率高:失败自动保留截图、视频、Trace 三件套,问题定位从「看日志」变为「看回放」
- 场景覆盖全:内置 Mock 机制,可覆盖后端未实现、异常难模拟的边界场景
- 隔离性优秀:浏览器上下文隔离,登录态与非登录态互不干扰,用例无耦合
- 低学习成本:保留 Playwright 原生 API 与 Pytest 使用习惯,团队上手快
六、总结
这套 Playwright+Pytest 的 UI 自动化框架,既吸收了 Playwright 在技术层面的先进性,又通过 POM 分层、Fixture 设计、Mock 体系、报告集成完成了工程化落地。它不仅适用于中小型项目的回归测试,也可通过扩展支持多环境执行、分布式运行、CI/CD 集成等企业级场景。
对于正在从 Selenium 转向 Playwright,或者想要搭建规范化 UI 自动化体系的团队,这套架构可以作为参考模板,根据自身业务进行裁剪与扩展,快速构建起高效、稳定、可维护的 Web 自动化测试体系