☰
pytest+playwright框架完善:从脚本到可维护的UI自动化架构
2026/10/11 22:39:53 网站建设 项目流程

先说个背景,这个系列已经写了两篇,前两篇我们把pytest和playwright的基本玩法捋了一遍,从元素定位到断言,从playwright的安装到与pytest的简单集成,算是把"能跑起来"这件事解决了。这一篇完全不一样,定位是"结合项目进行框架的完善"。

说白了,前两篇是讲怎么用工具,这一篇是讲怎么把工具组织成一个像样的、能经受住真实项目折腾的测试框架。我会以自己最近在做一个后台管理系统的UI自动化项目为例,把框架的目录结构、config管理、日志接入、失败重试、数据驱动、Page Object落地这些事一件件拆开讲,每一步都会给出实际用过的代码和踩坑记录,而不是把那些官方demo里的hello world再搬一遍。

如果你是刚接触pytest和playwright,前两篇没看的话建议先补一下基础;如果你已经能写一些零散的自动化脚本,但总觉得脚本越堆越多、越改越痛苦,那这篇应该正好对着你的痛点。

1. 为什么要在这个阶段谈"框架完善"

1.1 自动化测试的演进路径:脚本、框架、平台

很多团队做UI自动化的路径是高度相似的。刚起步时,写几个脚本验证核心流程能不能自动化,这时候大家关心的是"元素能不能定位到""点击之后页面能不能跳转",本质上是在验证playwright这个工具链是否靠谱。脚本写多了以后,问题就变了,变成"改一个页面跳转逻辑,我得上百个用例里去找那几处硬编码的URL""用例跑到一半失败,截图日志都得去CI里翻半天""新同事接手,看着目录完全不知道从哪里下手"。

这就是从脚本阶段进入框架阶段的信号。所谓框架,不是为了听起来高级,而是为了解决几个非常实际的问题:用例之间如何共享浏览器实例、如何统一管理配置和日志、如何把页面操作和业务断言解耦、如何让失败时能自动收集足够的现场信息。再往上走是平台化,就是把用例管理、执行、报表都做成web服务,但那已经是另一个话题了。这一篇聚焦在框架完善,目标是把你的自动化项目从"能跑"提升到"稳定、可维护、可扩展"。

1.2 框架完善的三个核心维度

我在完善这个框架时,脑子里始终有三把尺子。

第一把尺子是稳定性。UI自动化最大的敌人就是不稳定,同样的脚本今天跑过明天就挂,而且挂在不同的地方。解决稳定性的手段包括合理的等待策略、失败自动重试、准确的日志和截图记录。没有这些,脚本跑出来的报错信息根本没法定位问题,大家就会自然地说"UI自动化不靠谱"。

第二把尺子是可维护性。这体现在代码组织上。页面对象模型把页面元素定位和操作封装到独立的类里,用例层只关注业务场景;数据驱动把测试数据从代码里抽离出来,一个用例可以喂多组数据;公共方法抽到基类里,避免到处重复代码。维护性差的框架,改动成本会随着用例数量线性上升,最后重写比维护更便宜。

第三把尺子是可扩展性。新加一个页面,不需要动框架本身的代码;新加一个浏览器类型,改一行配置就能跑;需要接入新的报告平台,不至于被现有logger耦合死。框架的扩展性决定了它能跟你项目一起活多久。

这三把尺子,在实际项目中逐步落实。下面是我们在重构中每一步的具体做法。

2. 项目驱动的框架重构:目录结构与分层思想

2.1 从"脚本文件堆"到"分层架构"

我刚接手这个后台管理系统的自动化项目时,它的结构很"自由":一个test目录下面躺了几十个.py文件,每个文件里既有元素定位、又有业务操作、还直接print断言结果。更崩溃的是,很多URL和账号密码都散落在各个文件里,登录方式改了,全量脚本要跟着改。这种结构在用例少于20个的时候还能忍,一旦要覆盖完整的回归用例集,就完全失控。

重构的第一步,就是先确定分层思想。这套思路不是我拍脑袋想出来的,参考的是行业里成熟的做法,核心原则是"稳定层依赖不稳定层"的反向理解,也就是说,把最容易变化的部分(页面元素、操作步骤、测试数据)和相对稳定的部分(用例编排、报告、日志)分开存放,让变化的部分可以独立修改,不影响整体框架。

在pytest+playwright项目里,我常用的分层是四层:

  • 用例层(testcases),只负责业务场景的编排和断言;
  • 业务操作层(pages),封装页面对象和公共操作;
  • 数据层(data),存放测试数据和配置文件;
  • 基础设施层(common或core),处理pytest的fixture、配置读取、日志、报告、驱动实例化。

这样做的好处非常直接:当你需要改登录逻辑,你只需要去pages里改login_page.py;当你需要加一条测试数据,去data目录加一个参数化文件;当一个用例挂了,你看日志和截图能知道是页面元素变了还是业务规则变了。

2.2 核心目录结构说明

我最终用的目录结构是这个样子,你可以根据自己的项目调整:

ui_auto_test/ ├── pytest.ini # pytest主配置 ├── requirements.txt ├── config/ │ ├── config.yaml # 全局环境配置 │ └── settings.py # 配置读取封装 ├── core/ │ ├── base_page.py # 页面对象基类 │ ├── browser.py # 浏览器实例管理 │ ├── logger.py # 日志封装 │ └── screenshot.py # 截图公共方法 ├── pages/ │ ├── login_page.py │ ├── home_page.py │ └── user_manage_page.py ├── testcases/ │ ├── conftest.py # 核心fixture定义 │ ├── test_login.py │ └── test_user_manage.py ├── data/ │ ├── login_data.yaml │ └── user_data.json ├── reports/ # allure报告输出 ├── logs/ # 运行日志 └── utils/ └── read_data.py # 数据文件解析方法

这个结构里有几个细节值得说一下。第一,conftest.py放在了testcases目录而不是项目根目录,这样fixture的作用范围就限定在testcases这一层,不会影响其他潜在的测试目录,隔离性更好。第二,config目录和core目录分离,配置是静态数据,core是读写配置和实例化对象的逻辑,改配置不用动代码,改读取逻辑不会误碰数据文件。第三,pages目录下文件名与页面一一对应,比如login_page.py对应登录页,新接手的人看到目录基本就能猜出业务结构。

2.3 从"能跑"到"能维护"的关键转变点

说实话,目录结构是最容易抄的,真正难的是代码职责的边界划分。我在重构过程中定了两条硬性规矩,项目组里所有人都得遵守。

第一条规矩:测试用例里不允许直接出现playwright的API调用,比如page.click、page.fill这些。用例层只能调用pages层封装好的业务方法,比如login_page.login("username", "password")。这条规矩极度重要,它的价值体现在:有一天底层定位方式从CSS选择器改成XPath,所有用例代码一行都不用动,只需要改pages层对应的那个方法。

第二条规矩:元素定位表达式只允许出现在pages层,不允许出现在测试数据文件里。原因很简单,测试数据应该是业务层面的输入和期望结果,比如一个用户名、一个期望的提示信息;而元素选择器是实现层面的东西,比如"#login-btn"或者"text=登录"。把它们混在一起,会导致数据文件对页面结构高度敏感,页面一改,数据和脚本要一起改,这违背了数据驱动的初衷。

这两条规矩执行了大概两周,效果非常明显。后期新增用例的耗时下降了很多,新同事也不需要理解整个框架内部实现,按pages层的方法往上堆业务即可。

3. 基础设施完善:配置管理、日志与报告

3.1 多环境配置文件管理

在实际项目里,你几乎不可能只有一个测试环境。开发环境、测试环境、预发布环境,甚至你本地的环境,URL、账号、数据库连接串都不一样。之前脚本里写死URL的方式就不用提了,我在重构时把配置做了统一管理。

我选了YAML格式存配置文件,主要因为可读性好,嵌套结构也清晰。config.yaml大致长这样:

base_url: "https://test.example.com" browser: browser_type: "chromium" headless: false viewport: width: 1920 height: 1080 timeout: 30000 slow_mo: 500 account: admin: username: "admin" password: "admin123" normal: username: "user01" password: "user123"

针对不同环境,我用了一个很土但很有效的方式,通过环境变量指定加载哪个配置文件,默认加载config.yaml,如果设置了"RUN_ENV=staging"就加载config_staging.yaml。这比在代码里做复杂的if-else要直观得多。

配置读取的封装settings.py核心代码大致是这样的:

import os import yaml class Settings: _config = None @classmethod def load(cls, env=None): env = env or os.getenv("RUN_ENV", "test") config_file = os.path.join( os.path.dirname(__file__), f"config_{env}.yaml" if env != "test" else "config.yaml" ) with open(config_file, "r", encoding="utf-8") as f: cls._config = yaml.safe_load(f) @classmethod def get(cls, key, default=None): if cls._config is None: cls.load() keys = key.split(".") value = cls._config for k in keys: value = value.get(k, None) if value is None: return default return value

这样在代码里调用Settings.get("browser.timeout")就能拿到配置值,而且配置是运行时加载,不需要改代码就能切换环境。这个封装非常简单,但它解决了自动化项目里最烦人的环境混乱问题。

3.2 日志系统搭建

UI自动化的日志系统经常被低估。很多人觉得print一下就够了,但当你跑一条包含50个步骤的用例时,print出来的信息根本没有层次,你无法快速定位是第几步出了问题,更没法把日志和playwright的页面操作对应起来。

我用Python标准库logging,做了一个轻量封装,核心配置为:

import logging import os from datetime import datetime def setup_logger(name="ui_auto", log_level=logging.INFO, log_dir="logs"): os.makedirs(log_dir, exist_ok=True) logger = logging.getLogger(name) if logger.handlers: return logger logger.setLevel(log_level) log_file = os.path.join( log_dir, f"ui_auto_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log" ) fh = logging.FileHandler(log_file, encoding="utf-8") fh.setLevel(logging.INFO) sh = logging.StreamHandler() sh.setLevel(logging.INFO) fmt = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s" ) fh.setFormatter(fmt) sh.setFormatter(fmt) logger.addHandler(fh) logger.addHandler(sh) return logger

这个日志系统有几个比较实用的小设计。第一,文件名带时间戳,每次运行生成独立的日志文件,不会越跑越大,排查问题也方便跟具体的运行时间对应上。第二,日志格式里包含了文件名和行号,等查日志时候就能直接定位到是哪一行的代码打出来的,非常实用。第三,logger有handlers判断,避免重复添加handler,这在pytest的fixture中反复调用时特别重要。

在实际项目里,我要求pages层里面的公共方法都写入关键信息。登录成功时打一条"login success with user: xxx",断言失败时打一条"assert failed, actual value is xxx"。这样排查问题的时候,打开日志就能还原整个操作轨迹,再配合截图,基本上能还原现场。

3.3 测试报告集成与截图机制

报告这块,我选了Allure。原因很简单,项目组成员都习惯看Allure的报告,而且它支持步骤分组、附件(截图、日志)挂载,pytest集成又成熟。

集成方式就是pip安装allure-pytest,然后在pytest.ini里加一行addopts:

[pytest] addopts = -v --alluredir=./reports/allure-results --clean-alluredir

真正有价值的不是这个基础配置,而是自动截图机制。UI自动化中,用例失败瞬间的页面状态是最重要的排查线索。我在conftest.py里写了一个hook,用例失败时自动截图并附加到Allure报告中:

import allure import pytest from core.screenshot import take_screenshot @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: page = item.funcargs.get("page", None) if page: screenshot = take_screenshot(page, item.name) allure.attach.file( screenshot, name="failure_screenshot", attachment_type=allure.attachment_type.PNG )

截图方法take_screenshot封装在core/screenshot.py里:

import os from datetime import datetime def take_screenshot(page, prefix="failure"): os.makedirs("screenshots", exist_ok=True) filename = os.path.join( "screenshots", f"{prefix}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" ) page.screenshot(path=filename, full_page=True) return filename

这个机制带来的体验提升是巨大的。你去Allure报告里看一个失败的用例,能直接看到失败那一刻整页截图,而不是只有一段孤零零的堆栈。如果你的页面很长,可以设置full_page=True截全屏,我测试过后台管理系统时喜欢用全屏截图,因为很多表格操作错误要到页面底部才能看到关键信息。

4. 关键机制实现:fixture、重试与数据驱动

4.1 conftest.py 中的核心fixture设计

pytest的fixture是整个框架的粘合剂,playwright的浏览器实例、配置对象、登录状态、公共接口,都应该通过fixture生成。下面是我在conftest.py里核心的fixture设计:

import pytest from core.browser import BrowserManager from core.logger import setup_logger from config.settings import Settings logger = setup_logger() @pytest.fixture(scope="session") def browser_context(): manager = BrowserManager() context = manager.new_context() yield context manager.close() @pytest.fixture() def page(browser_context): page = browser_context.new_page() yield page page.close() @pytest.fixture() def login_page(page): from pages.login_page import LoginPage return LoginPage(page)

这里有两个设计点说一下。第一,browser_context的scope是session,也就是说整个测试会话只启动一个浏览器context,每个用例独立开新页面,既快又隔离。第二,page和login_page的scope都是function,保证每个用例拿到的是干净页面,用例之间互不污染。

登录态的复用是UI自动化里提升速度的核心。每个用例都从登录页输入账号密码走一遍,100个用例就是100次登录,时间和网络开销都不小。我用storage_state来实现登录态的会话级复用:

@pytest.fixture(scope="session") def auth_state(browser_context): context = browser_context login_page = LoginPage(context.new_page()) login_page.login(Settings.get("account.admin.username"), Settings.get("account.admin.password")) state = context.storage_state() context.clear_cookies() return state @pytest.fixture() def page(browser_context, auth_state): context = browser_context context.add_cookies(auth_state["cookies"]) page = context.new_page() yield page page.close()

这样第一个用例执行时完成登录,后续用例直接复用cookie,不需要重新登录。这里要注意storage_state保存的cookies有时候会带上不该带的东西,比如本地的开发环境标识,所以我在生成state之后主动clear_cookies,再按需添加,避免脏数据污染。

4.2 用例失败自动重试机制

UI自动化的脆弱性决定了失败重试是刚需。我用的工具是pytest-rerunfailures插件,安装完之后在pytest.ini里配置:

addopts = -v --alluredir=./reports/allure-results --clean-alluredir --reruns 2 --reruns-delay 1

意思是最多重试2次,每次重试间隔1秒。这里必须说一个我的心酸教训:重试并非越多越好,之前项目里设置了5次重试,用例跑下来耗时翻倍,而且某些用例在重试期间反复操作同一个按钮,把测试环境的脏数据搞出来一堆。最终确定了"非关键用例重试2次、重要业务用例不重试"的规则,稳定性反而更高。如果一个重要用例跑挂了,你要的应该是准确的失败现场,而不是它在后台偷偷重试之后假装成功。

另外,重试和截图机制要配合好。pytest-rerunfailures默认在重试成功后会把之前的失败信息吞掉,但截图是hook里生成的,仍然会附着到最后的Allure报告里,看起来就像成功用例带了一张失败截图,容易误导人。我后来在截图文件命名上加了时间戳,并且在截图前打一条warning日志,基本能区分是哪一次运行留下的。

4.3 数据驱动与参数化

UI自动化里的数据驱动,很多人只是简单用一下parametrize,但我项目里遇到的真实需求要多一些:字段组合多、数据量大、不同用例需要不同数据文件。我的做法是把测试数据写到YAML/JSON文件,由工具方法读取后返回列表,再喂给parametrize。

data/login_data.yaml长这样:

login_success: - case: "valid admin login" username: "admin" password: "admin123" expect: "欢迎回来,admin" - case: "valid normal user login" username: "user01" password: "user123" expect: "欢迎回来,user01" login_fail: - case: "wrong password" username: "admin" password: "wrongpass" expect: "用户名或密码错误" - case: "empty username" username: "" password: "admin123" expect: "用户名不能为空"

然后在用例里这样用:

import pytest from utils.read_data import load_yaml_data data = load_yaml_data("data/login_data.yaml") @pytest.mark.parametrize("username,password,expect", data["login_success"]) def test_login_success(page, username, password, expect): ...

load_yaml_data是一个很简单的封装:

import yaml import os def load_yaml_data(path): with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f)

这里要提醒一个坑:parametrize的参数列表是静态的,在用例导入时就会执行,所以load_yaml_data不能依赖运行时才生成的配置或环境变量,否则很容易报"参数化数据读取失败",而且这个报错信息还特别难排查。

4.4 用例依赖与执行顺序控制

很多刚入门的人喜欢让用例之间有依赖,比如test_b必须先执行,test_a执行成功之后它才执行。在pytest里用--durations控制执行耗时,用pytest-order插件控制用例顺序,但我强烈建议你尽量避免用例间依赖。UI自动化的用例应该是独立的,因为一旦你设置了顺序依赖,某个用例失败就会导致下游一串用例失败,排查成本剧增。

如果你确实需要控制用例执行顺序,比如先跑冒烟测试再跑回归测试,可以这么做:

pytest testcases -m smoke pytest testcases -m regression

用mark来区分优先级,比在代码里硬编码顺序要优雅得多。在用例上这样打标:

@pytest.mark.smoke def test_login_and_logout(page): ...

在pytest.ini里注册mark:

markers = smoke: 冒烟用例 regression: 回归用例

这样整个执行策略就被你拿捏在了手里。回归时只跑regression,冒烟时只跑smoke,都不需要改动任何用例代码。

5. 代码实战:页面对象模型与业务封装

5.1 Page Object 模式的落地

Page Object Model是UI自动化里最经典的封装模式,核心思想把一个页面的元素定位和业务操作封装到一个类里,测试用例只跟业务方法打交道,不直接操作选择器。

base_page.py我做了这样一个基类:

from core.logger import setup_logger logger = setup_logger() class BasePage: def __init__(self, page): self.page = page def navigate_to(self, url): logger.info(f"navigate to {url}") self.page.goto(url) def click(self, selector, **kwargs): logger.info(f"click element: {selector}") self.page.click(selector, **kwargs) def fill(self, selector, text, **kwargs): logger.info(f"fill element: {selector}, text: {text}") self.page.fill(selector, text, **kwargs) def get_text(self, selector, **kwargs): text = self.page.text_content(selector, **kwargs) logger.info(f"get text: {selector}, return: {text}") return text def is_visible(self, selector, **kwargs): return self.page.is_visible(selector, **kwargs)

你可能会觉得这几个方法没什么技术含量,不就是包了一层嘛。但它带来的两个好处值得说说。第一,所有日志在这里统一输出,每次操作都有痕迹,排查问题时不需要在几十个用例里猜。第二,如果playwright升级后某个API废弃了,或者你决定从playwright换成其他工具,只需要改base_page这些公共方法,用例层的代码全部不动。

拿login_page.py来说:

from core.base_page import BasePage class LoginPage(BasePage): username_selector = "#username" password_selector = "#password" login_btn_selector = "#loginBtn" error_tip = ".el-form-item__error" def login(self, username, password): self.fill(self.username_selector, username) self.fill(self.password_selector, password) self.click(self.login_btn_selector) def get_error_message(self): return self.get_text(self.error_tip) def login_success_expect_text(self): return "欢迎回来"

这里面就把"登录这个动作"封装成了一个稳定的业务方法,元素定位如果在某个版本变了,改这一处就好。

5.2 复杂业务场景的封装示例

页面对象不只是把点击填充包一层,更重要的是把复杂的业务操作串起来。我项目里有一个比较典型的场景:在用户管理页面新增一个用户,需要填写表单、选择角色、上传头像、点击保存,然后校验列表里出现新用户。

这个场景如果写在用例里会非常长,而且每一步的细节,比如选择器、操作顺序,都属于页面实现细节,不该让用例层看到。我把它封装成user_manage_page.py的一个方法:

class UserManagePage(BasePage): add_user_btn = "#addUserBtn" name_input = "#userName" role_select = "#roleSelect" avatar_upload = "#avatarInput" save_btn = "#saveBtn" search_input = "#searchInput" search_btn = "#searchBtn" table_rows = ".el-table__row" def create_user(self, name, role, avatar_path): self.click(self.add_user_btn) self.fill(self.name_input, name) self.select_option(self.role_select, role) self.page.set_input_files(self.avatar_upload, avatar_path) self.click(self.save_btn) toast = self.get_text(".el-message--success") assert "新增成功" in toast def search_user(self, name): self.fill(self.search_input, name) self.click(self.search_btn) return self.get_text(self.table_rows)

这样用例层就非常清爽:

def test_create_and_search_user(user_manage_page): user_manage_page.create_user("zhangsan", "admin", "data/avatar.png") assert "zhangsan" in user_manage_page.search_user("zhangsan")

这种封装让用例更像一份"操作说明书",而不是一串技术细节集合。你甚至可以让测试人员直接按照用例的步骤去手工复现,因为他们看得懂这些业务方法名。

6. 常见问题与排查技巧实录

6.1 元素定位失败:iframe、shadow DOM、动态属性

UI自动化里80%以上的失败都跟元素定位有关。我项目中遇到最多的三类情况,这里把排查方法一并写了。

iframe场景:后台管理系统很多都嵌入了旧的iframe页面。playwright里处理iframe的方式比较优雅,用frame_locator接口:

frame = page.frame_locator("#mainFrame") frame.locator("#btnInFrame").click()

不直接定位iframe里面元素的原生选择器,而是先声明frame_locator,再在这个上下文里定位,这让代码的意图清晰很多。

shadow DOM场景:某些表单控件内部是shadow DOM结构,普通CSS选择器进不去。playwright也支持穿透shadow DOM,用CSS里的深度选择器可以搞定。这种情况下,我一般会把shadow DOM里的元素定位封装到pages层,用例层完全无感。

动态属性场景:很多前端框架会在每一次渲染时给元素生成随机ID,比如id="btn-12345"这种。这时候不能硬用id去定位,要么用相对稳定的class或data属性,要么用文本定位。playwright的text=定位器非常好用,特别是在按钮文案不容易变的场景下。

6.2 等待策略使用不当导致的稳定性问题

playwright最强大的地方之一是它的自动等待机制,元素可交互时才执行下一步操作。但自动等待解决不了所有问题,特别是接口返回和页面渲染不同步时。

我遇到一个经典问题:点击保存按钮后,页面上先弹出一个"加载中"的遮罩,遮罩消失后才显示成功提示。自动等待对"遮罩存在"的判定不够精确,直接去读成功提示会扑空。解决办法是显式隐藏等待:

def wait_mask_hidden(self): self.page.wait_for_selector(".loading-mask", state="hidden", timeout=10000)

把这些等待逻辑也封装到pages层方法中,不要散落在用例里。

另外一个经验是,playwright默认的timeout是30秒,这个数值在绝大多数场景下太长。如果页面确实挂了,30秒的等待会让整个测试套件变得不可容忍。我建议在浏览器context初始化时把这个值调成10秒或者15秒,让失败来得快一点,早暴露早修复。

6.3 并发执行下的环境隔离问题

UI自动化上并发,多数人一开始都想跑得越快越好。pytest-xdist就是干这个的。但在真实项目里,并发执行对测试环境有非常高的要求。如果你要并发执行,必须保证:

  • 测试环境能承受多个浏览器同时操作;
  • 测试数据之间没有互相覆盖的可能;
  • 每个worker进程的日志和截图输出不会写到同一个文件。

我项目里前期并发跑的时候,最典型的问题是多个worker同时操作同一个用户的登录状态,导致session混乱,用例全部失败。后来我改为给每个worker分配不同的测试账号,或者使用无共享状态的独立用户,问题才消停。

如果你们环境不具备并发条件,别硬上,串行跑完整套回归,加上合理的重试和日志,其实也够用。我见过不少团队为了并发而并发,最后投入产出比惨不忍睹。

6.4 其他踩坑记录

最后补几个小坑。

一是关于first/last元素的处理。playwright的locator.first 可以直接用,但要注意first本身也是一个locator,不是点击之后返回什么。如果你需要操作多个匹配元素中的某一个,不要用page.click直接传一个会命中多个元素的定位表达式,playwright会报strict mode violation。它的报错信息很明确,说"strict mode violation",解决方法就是locator.first或者locator.nth(0)明确指定。

二是关于本地存储和cookie的坑。storage_state保存下来的是所有域的cookie,如果你的测试环境有多个子域,后续登录态复用时候可能会带上一些失效的域名cookie,导致登录跳转异常。建议在保存state之前,清理掉与本次测试无关的domain,或者只保留目标根域名的cookie。

三是关于慢速操作。本地调试时建议把browser的slow_mo设置为200到500毫秒,这样能看到playwright每一步操作过程,定位问题非常直观。CI环境里则把slow_mo设成0,追求速度。

四是我个人的一个小习惯:凡是用到页面元素定位的地方,我都会在pages层的方法里加一个日志,把用的选择器和关键参数打印出来。这样不仅排查问题快,而且当你去优化定位方式时,日志能帮你确认现在跑的到底是哪条路径。

这套框架到目前为止,我已经在项目里跑了三个月,覆盖了六个典型业务模块,回归用例量从最初的30条涨到了160条左右,执行时间从早期的25分钟压到了13分钟(串行+登录态复用)。稳定性上,最近的完整回归成功率基本维持在95%以上,剩下的失败大多来自前端改动引发的定位失效,这部分靠截图和日志能很快定位,修起来也就是改一个selector的事。

最后再分享一个个人很深的体会:框架完善这件事没有终点,它是跟着项目走的。项目页面在变,业务逻辑在变,测试环境和数据也在变,框架只有在这个持续变化的过程中不断被调整,才有存在的价值。不要为了某个看起来很"规范"的架构而不愿意修改它,也不要因为某个脚本写得丑就一直拒绝动手,两者都会让自动化体系悄悄烂掉。我觉得最好的状态是,框架的每一次调整都能减少一点维护成本、提升一点排查效率,这就值了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询