news 2026/10/1 5:14:22

PO+Selenium+unittest:可维护的Web自动化测试框架实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PO+Selenium+unittest:可维护的Web自动化测试框架实战

1. 为什么这套组合值得投入:PO + Selenium + Unittest 的选型逻辑

做过几年 Web 自动化的人大概都有过这个阶段:一开始图快,脚本里全是driver.find_element(By.ID, "username").send_keys("admin"),一个文件写完登录、下单、查询全流程。跑通的那一刻挺爽,但等到页面改了一个按钮的 id,或者需要把同一套流程跑在三套环境上,维护成本就会像滚雪球一样压过来。PO(Page Object,页面对象)模式 + Selenium + unittest 这套组合,本质上就是用来治这个病的。它不是什么新潮技术栈,但在中小型团队、以回归验证为主、需要长期维护的项目里,它的性价比依然很难被替代。这篇文章我会把整套项目的搭建过程、每一步为什么这么设计、踩过的坑和排查技巧完整拆出来,适合刚接触自动化测试、想从"能跑"进阶到"能维护"的同学,也适合已经在写脚本但被维护成本折磨的同行。

1.1 PO 模式真正解决的三个痛点

很多人对 PO 的理解停留在"每个页面写一个类",这其实只说对了一半。PO 的核心是把"页面长什么样"和"要做什么操作"分开,让元素定位信息和业务操作逻辑各归其位。

第一个痛点是定位信息的散落。在没有 PO 的项目里,一个登录按钮的定位可能在 20 个用例里重复出现。产品经理心情好换了个 class 名,你得全文搜索改 20 处,改漏一处就是一片飘红。PO 把定位信息收敛到页面类的一个属性上,页面变了只改一处,用例层完全无感。

第二个痛点是业务语义缺失。element.click()这种写法三个月后自己都看不懂点的是什么。而login_page.click_submit()一眼就知道在提交登录表单。用例应该读起来像业务说明书,而不是一堆底层 API 的堆砌。

第三个痛点是复用与扩展。同一个"登录"动作,可能被下单流程、退款流程、报表流程各调用一次。PO 把它封装成一个方法后,任何新流程都能直接复用,新人接手时也不用重新理解一遍页面结构。

注意:PO 不是"一个页面必须一个类"。弹窗、抽屉、公共导航栏这类跨页面复用的组件,完全可以单独抽成一个 Component 类,被多个页面类组合使用。死守"一页一类"反而会导致大量重复代码。

1.2 为什么很多团队依然选 unittest 而不是 pytest

pytest 生态确实更好,插件丰富、断言优雅、fixture 强大。但 unittest 有两个无法忽视的现实优势。

一是标准库自带,不需要额外引入第三方依赖。在有些公司的内网环境里,装个第三方包要走审批流程,而 unittest 直接import就能用,这对落地速度的影响比想象中大。二是与既有 CI 和测试平台的兼容性。很多公司的测试管理平台、Jenkins 流水线、报告解析脚本,默认就是按 unittest 的输出格式和用例组织方式来解析的,换成 pytest 反而要改一堆周边设施。

当然,unittest 的短板也很明确:断言不够优雅(没有裸assert,得记assertEqual、assertTrue一大堆)、参数化不如 pytest 灵活(得靠 ddt 补)、fixture 能力弱。我的做法是主体用 unittest,参数化用 ddt,报告用 HTMLTestRunner 或 BeautifulReport,用最小的依赖代价补齐短板。如果团队已经在用 pytest 且没有兼容包袱,那直接用 pytest 也没问题,PO 的架构思路是完全通用的。

1.3 三层架构的分工边界

整套项目我划分为三层,边界必须清晰,不然很容易写成"四不像"。

层级职责典型文件禁止出现的内容
基础层 Base驱动封装、等待封装、日志、截图、配置读取base_page.py、driver_factory.py任何具体业务语义
页面对象层 Pages元素定位、页面级操作、页面内断言login_page.py、order_page.py跨页面的业务流程
用例层 Tests组织业务流程、调用页面方法、最终断言test_login.py任何find_element调用

这张表里的第三列"禁止出现的内容"是我踩过坑后加的硬规矩。最早我在用例层里直接写了定位,结果页面一改,用例层和页面层一起改,PO 的意义直接归零。后来在代码评审里把这条当红线卡住,维护成本立刻降下来。判断标准很简单:如果用例文件里出现了By.或者find_element,这个用例就不合格。

三层之外还有两个横向支撑模块:config(环境地址、账号、超时时间等)和utils(日志、截图、随机数据生成、数据库校验工具)。它们不属于任何一层,被各层按需调用,这样既避免了循环依赖,也方便单独测试工具方法本身。

再说说业务流程的位置。像"登录 → 搜索商品 → 加购物车 → 下单 → 支付"这种跨多页面的长流程,我倾向于放在用例层,或者单独抽一个business包做成流程类。放用例层的好处是流程一目了然,缺点是多个用例要复用同一段流程时会有重复。用流程类的好处是复用性好,缺点是多了层跳转,调试时不够直观。小项目我建议直接放用例层,等真的出现第三个用例复用同一流程时再抽出来,不要为了架构而架构。

2. 项目骨架搭建:从目录设计到驱动管理

架构思路理清了,接下来是落地。搭骨架这件事,很多人觉得随便建几个文件夹就行,但目录结构一旦定型,后面所有代码都要往里面塞,改起来牵一发动全身。我习惯在动手写第一行代码前,先把目录结构和各文件的职责写在纸上确认一遍,这个习惯至少帮我省过三次大规模重构。

2.1 目录结构怎么划分才不返工

下面是我在多个项目里迭代出来的结构,中小型项目可以直接抄:

auto_test_project/ ├── config/ │ ├── config.yaml # 环境地址、超时、账号等配置 │ └── config_reader.py # 配置读取封装 ├── common/ │ ├── base_page.py # 页面基类:等待、查找、点击、输入 │ ├── driver_factory.py # 浏览器驱动初始化与销毁 │ ├── logger.py # 日志封装 │ └── screenshot.py # 失败截图工具 ├── pages/ │ ├── login_page.py │ ├── home_page.py │ ── order_page.py ├── testcases/ │ ├── test_login.py │ ├── test_order.py │ └── conftest_data/ # 数据驱动用的 yaml / excel ├── testdata/ │ ├── login_data.yaml │ └── order_data.yaml ├── reports/ # 测试报告输出 ├── logs/ # 日志输出 ├── screenshots/ # 截图输出 ├── run.py # 统一入口 ── requirements.txt

几个关键决策解释一下。第一,config单独成目录而不是塞在 common 里,是因为配置是横切关注点,所有层都要读,单独放语义更清晰。第二,testdata和testcases分开,是因为数据要能被非技术人员编辑,YAML 比 Python 文件友好太多,测试同学改个账号不用碰代码。第三,reports、logs、screenshots全部走独立目录并在.gitignore里忽略,避免仓库被产物文件撑爆。

提示:config.yaml里绝对不要写生产环境的真实账号密码。我一般只在配置里放测试环境账号,敏感信息通过环境变量注入,config_reader.py读取时优先取环境变量,取不到再用默认值。这个习惯在代码审计时能省很多解释成本。

2.2 配置管理与浏览器驱动初始化

配置读取我封装了一个简单的单例,避免每次读文件:

# config/config_reader.py import os import yaml class ConfigReader: _instance = None _config = None def __new__(cls, path=None): if cls._instance is None: cls._instance = super().__new__(cls) base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) default_path = os.path.join(base_dir, "config", "config.yaml") with open(path or default_path, "r", encoding="utf-8") as f: cls._config = yaml.safe_load(f) return cls._instance def get(self, key, default=None): return self._config.get(key, default)

驱动初始化放在driver_factory.py里统一管理,这地方有个必须讲清楚的坑:不要在模块顶层创建 driver。我见过有项目在base_page.py顶部写了driver = webdriver.Chrome(),结果所有用例共用一个浏览器实例,用例之间互相污染,上一个用例遗留的登录态直接让下一个用例失败。

正确做法是用工厂方法按需创建:

# common/driver_factory.py from selenium import webdriver from selenium.webdriver.chrome.options import Options from common.config_reader import ConfigReader def create_driver(): cfg = ConfigReader() browser = cfg.get("browser", "chrome") headless = cfg.get("headless", False) if browser == "chrome": options = Options() if headless: options.add_argument("--headless=new") options.add_argument("--window-size=1920,1080") options.add_argument("--disable-gpu") driver = webdriver.Chrome(options=options) else: raise ValueError(f"暂不支持的浏览器类型: {browser}") driver.implicitly_wait(0) driver.maximize_window() return driver

注意这里implicitly_wait(0),也就是显式关闭隐式等待。隐式等待和显式等待混用是自动化测试里最经典的一个坑:两者叠加时实际等待时间是不可预测的,有时会远超你设定的超时值,导致用例莫名其妙变慢。我的策略是全局只用显式等待,隐式等待一律设为 0。

关于 selenium 安装,pip install selenium就够了,4.x 版本开始 Selenium Manager 会自动处理驱动下载,不再需要手动下载 chromedriver 放进 PATH。如果公司内网无法访问外网,可以用pip config set global.index-url配置内部镜像源,或者提前把驱动放到指定目录,通过Service(executable_path=...)显式指定。判断驱动的浏览器主版本是否匹配,用chrome://version/看主版本号,和驱动主版本一致即可,小版本不必强求对齐。

2.3 BasePage 基础层封装

BasePage 是整个项目的发动机,所有页面类都继承它。它要做的事情是:把定位、等待、点击、输入、取值这些高频操作统一封装,并加日志和截图钩子。

# common/base_page.py import os import time from datetime import datetime from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException from common.config_reader import ConfigReader from common.logger import get_logger logger = get_logger(__name__) class BasePage: def __init__(self, driver): self.driver = driver self.timeout = ConfigReader().get("timeout", 10) def find(self, locator, timeout=None): """等待元素可见后返回,locator 形如 (By.ID, 'kw')""" timeout = timeout or self.timeout try: return WebDriverWait(self.driver, timeout).until( EC.visibility_of_element_located(locator) ) except TimeoutException: self.save_screenshot("find_timeout") logger.error(f"元素定位超时: {locator}") raise def click(self, locator, timeout=None): el = self.find(locator, timeout) self.driver.execute_script( "arguments[0].scrollIntoView({block:'center'});", el ) el.click() logger.info(f"点击元素: {locator}") def input_text(self, locator, text, timeout=None): el = self.find(locator, timeout) el.clear() el.send_keys(text) logger.info(f"输入文本到 {locator}: {text}") def get_text(self, locator, timeout=None): return self.find(locator, timeout).text def save_screenshot(self, tag=""): cfg = ConfigReader() base_dir = cfg.get("screenshot_dir", "screenshots") os.makedirs(base_dir, exist_ok=True) ts = datetime.now().strftime("%Y%m%d_%H%M%S") path = os.path.join(base_dir, f"{tag}_{ts}.png") self.driver.save_screenshot(path) logger.info(f"截图已保存: {path}") return path

这里有几个细节值得单独说。scrollIntoView那段是为了解决"元素存在但被固定头部遮挡导致点击失败"的问题,这个坑在带吸顶导航的页面上特别常见,报错信息还是ElementClickInterceptedException,不熟悉的人很难第一时间想到是被遮挡。截图钩子挂在find的超时分支上,保证任何定位失败都能留下现场照片,排查时不用靠猜。

还有一个经验:不要用time.sleep()。我在项目里有个不成文的规定,代码评审看到time.sleep就要问一句为什么。绝大多数情况下它能被WebDriverWait替代,而sleep的问题是它要么短了不够用,要么长了拖慢整个套件,几十个用例累积下来就是几分钟的浪费。

3. 页面对象层实操:元素定位与交互细节

骨架搭好,接下来是最见功力的页面对象层。这一层写得好不好,直接决定了项目三个月后还能不能维护。我见过太多项目在这一层堆了上百行定位语句,最后还是退化成了一锅粥。

3.1 元素定位元数据的集中管理

定位策略上,优先级我是这么排的:

定位方式推荐度适用场景风险点
ID高唯一标识的元素前端换框架时可能丢失
name高表单元素多表单页面可能重名
CSS Selector高大部分场景过度依赖层级易失效
># pages/login_page.py from selenium.webdriver.common.by import By from common.base_page import BasePage class LoginPage(BasePage): # 定位元数据集中在此,仅用于描述"元素在哪",不含操作逻辑 URL_PATH = "/login" USERNAME_INPUT = (By.CSS_SELECTOR, '[data-test="username"]') PASSWORD_INPUT = (By.CSS_SELECTOR, '[data-test="password"]') SUBMIT_BTN = (By.CSS_SELECTOR, '[data-test="login-submit"]') ERROR_TIP = (By.CSS_SELECTOR, '.error-tip') def open(self, base_url): self.driver.get(base_url.rstrip("/") + self.URL_PATH) return self def login(self, username, password): self.input_text(self.USERNAME_INPUT, username) self.input_text(self.PASSWORD_INPUT, password) self.click(self.SUBMIT_BTN) return self def get_error_message(self): return self.get_text(self.ERROR_TIP)

这个写法有个隐藏好处:整个页面的"元素地图"一眼看全。新人接手时先看这段常量列表,就知道这个页面有哪些可操作元素,比翻遍整个文件找定位语句高效得多。这种把定位元数据与操作逻辑分离的做法,最近在一些团队里被叫做"页面元素枚举"或"仅存储定位元数据",本质就是同一件事。

3.2 非原生下拉框(div+ul+li)的定位与选择

Select类只能处理原生<select>标签。但现在前端框架做出来的下拉框,绝大多数是<div><ul><li>组合,直接对它们用Select会抛UnexpectedTagNameException。这是新手最常卡住的地方之一。

麻烦点在于:这类下拉框的选项列表通常是点击后才渲染到 DOM 里,甚至挂在body下用绝对定位展示,跟触发按钮在 DOM 树上根本不是父子关系。所以定位选项时不能从下拉框容器往下找。

我封装了一个通用方法处理这类组件:

def select_custom_dropdown(self, trigger_locator, option_text, list_locator): """ trigger_locator: 下拉框触发按钮的定位 option_text: 要选择的选项文本 list_locator: 选项容器定位,一般在 body 下 """ self.click(trigger_locator) # 等待选项列表出现 self.find(list_locator) option_locator = ( By.XPATH, f"//li[normalize-space(text())='{option_text}']" ) self.click(option_locator) logger.info(f"已选择下拉项: {option_text}")

这里normalize-space(text())很关键。前端渲染的文本经常带前后空格或换行符,直接text()='北京'匹配不上,加上normalize-space自动去空白后就稳了。另外一个常见坑是选项文本里有动态内容,比如"北京(123 条)",这时候就得改用contains():

option_locator = (By.XPATH, f"//li[contains(text(), '{option_text}')]")

注意:用contains时要小心前缀相同的情况。比如同时存在"北京"和"北京市",contains(text(), '北京')会匹配到两个元素,click就会报元素不唯一的错。这种情况下建议在 XPath 里追加索引或者用更精确的匹配条件。

还有一类更刁钻的场景:下拉选项是虚拟滚动渲染的,也就是只渲染可视区域的几十个选项,滚动时才动态加载。这时候要先用 JS 滚动到目标位置,或者用搜索框先过滤缩小范围再点。这类页面我不太建议硬做全量选项遍历,性价比太低,聚焦业务主流程的几个关键选项就够。

3.3 显式等待的封装与超时策略

等待策略直接决定用例的稳定性。我的原则是:每一个会改变页面状态的操作之后,都必须有一个明确的等待条件,而不是等一个固定时间。

常用的等待条件归纳一下:

场景推荐条件说明
等待元素出现visibility_of_element_located元素可见,最常用
等待元素存在presence_of_element_located元素在 DOM 但可能不可见
等待元素可点击element_to_be_clickable按钮类元素首选
等待元素消失invisibility_of_element_located等 loading 遮罩消失
等待文本变化text_to_be_present_in_element等异步结果回填
等待 URL 变化url_contains等页面跳转完成

超时时间我分了三档:常规操作 10 秒,页面跳转和登录 20 秒,涉及第三方接口的查询类操作 30 秒。统一配置在config.yaml里,不同的等待调用可以传参覆盖。这样做的好处是环境慢的时候改一个配置就行,不用满代码找魔法数字。

另外,element_to_be_clickable我一般用在按钮上,visibility_of_element_located用在读取文本的元素上。用错的影响是:按钮可能"可见但不可点"(比如被透明遮罩盖住),这时候用可见性判断会提前返回,紧接着的 click 就会失败。

3.4 页面对象类的完整写法

把上面几点综合起来,一个生产可用的页面对象大概是这个样子:

# pages/order_page.py from selenium.webdriver.common.by import By from common.base_page import BasePage class OrderPage(BasePage): SEARCH_INPUT = (By.CSS_SELECTOR, '[data-test="search-input"]') SEARCH_BTN = (By.CSS_SELECTOR, '[data-test="search-btn"]') RESULT_LIST = (By.CSS_SELECTOR, '.result-list li') SUBMIT_ORDER_BTN = (By.CSS_SELECTOR, '[data-test="submit-order"]') CONFIRM_DIALOG = (By.CSS_SELECTOR, '.confirm-dialog') CONFIRM_OK = (By.CSS_SELECTOR, '.confirm-dialog .ok-btn') SUCCESS_TIP = (By.CSS_SELECTOR, '.toast-success') def search(self, keyword): self.input_text(self.SEARCH_INPUT, keyword) self.click(self.SEARCH_BTN) self.find(self.RESULT_LIST) return self def get_result_count(self): return len(self.driver.find_elements(*self.RESULT_LIST)) def submit_order(self): self.click(self.SUBMIT_ORDER_BTN) self.find(self.CONFIRM_DIALOG) self.click(self.CONFIRM_OK) return self def get_success_tip(self): return self.get_text(self.SUCCESS_TIP)

注意get_result_count用了find_elements而不是find,因为列表数量可能是 0,用find等待会直接超时。这种"允许为空"的场景是find_elements的典型用法,写的时候要能区分开。

4. 测试用例层:unittest 组织、数据驱动与断言

到了用例层,业务流程图终于能清晰呈现了。这一层的目标不是炫技,而是让任何一个懂业务的人读一遍就能确认流程对不对。

4.1 用例分层与 setUp/tearDown 的正确用法

unittest 提供了setUpClass、setUp、tearDown、tearDownClass四个钩子。用错层级会导致资源浪费或状态污染。

我的划分标准是:驱动和登录态放在setUpClass,用例级数据准备放在setUp,数据清理放在tearDown,驱动销毁放在tearDownClass。

# testcases/test_order.py import unittest from common.driver_factory import create_driver from common.config_reader import ConfigReader from pages.login_page import LoginPage from pages.order_page import OrderPage class TestOrder(unittest.TestCase): @classmethod def setUpClass(cls): cfg = ConfigReader() cls.driver = create_driver() cls.base_url = cfg.get("base_url") cls.order_page = OrderPage(cls.driver) # 一次登录,整个类复用 LoginPage(cls.driver).open(cls.base_url).login( cfg.get("user"), cfg.get("password") ) @classmethod def tearDownClass(cls): cls.driver.quit() def setUp(self): self.order_page.driver.get(self.base_url + "/order") def test_search_and_submit_order(self): self.order_page.search("测试商品") self.assertGreater(self.order_page.get_result_count(), 0) self.order_page.submit_order() self.assertIn("成功", self.order_page.get_success_tip())

为什么登录放setUpClass而不是每个用例都登一次?因为登录是重操作,涉及接口请求和页面跳转,每个用例登一次会让整个套件时间翻好几倍。但这么做的代价是用例之间会共享登录态,如果某个用例会清空会话或者切换账号,就会互相影响。遇到这种情况,我会把会污染状态的用例单独拆一个测试类,让它自己管自己的登录。

提示:setUp里的driver.get是每次用例都执行的,用get而不是点击导航,是因为get更快且不依赖导航菜单的稳定性。但要注意这会丢失前一个用例的前端状态,如果用例之间有前后依赖,就不能这么写。

4.2 数据驱动与用例参数化

同一套流程要用不同数据跑多遍时,ddt 是 unittest 生态里最顺手的方案:

import unittest from ddt import ddt, data, file_data, unpack from pages.login_page import LoginPage @ddt class TestLoginDataDriven(unittest.TestCase): @classmethod def setUpClass(cls): cls.driver = create_driver() cls.page = LoginPage(cls.driver) @file_data("../testdata/login_data.yaml") def test_login(self, username, password, expect): self.page.open("http://test.example.com").login(username, password) if expect == "success": self.assertIn("/home", self.driver.current_url) else: self.assertTrue(self.page.get_error_message())

对应的 YAML 数据:

- ["admin", "correct_pwd", "success"] - ["admin", "wrong_pwd", "fail"] - ["", "any_pwd", "fail"] - ["locked_user", "any_pwd", "fail"]

用 YAML 而不是 Excel 的理由很实际:YAML 是纯文本,能进 Git 做版本管理,能看 diff,合并冲突时也好处理。Excel 是二进制,版本管理基本失效,多人协作时还容易互相覆盖。

数据驱动最容易踩的坑是数据里的中文乱码。读取文件时务必显式指定encoding="utf-8",Windows 环境下默认编码可能是 GBK,读的时候不指定就会报UnicodeDecodeError。

还有一个坑:@file_data默认的路径是相对于当前测试文件所在目录,如果 YAML 放在项目根的testdata目录,路径里要带../。我一般会在测试文件顶部定义一个常量DATA_DIR,用os.path.join拼接绝对路径,彻底躲开相对路径的坑。

4.3 失败截图与日志埋点

用例失败时最想要的是现场信息。我的做法是在tearDown里判断用例结果,失败就自动截图:

def tearDown(self): if any(error for _, error in self._outcome.errors if error): self.driver.save_screenshot(f"screenshots/fail_{self._testMethodName}.png")

不同版本的 unittest 里获取用例结果的 API 略有差异,稳定一点的做法是重写run方法或者自定义TestResult类。我自己习惯写一个MyTestResult(unittest.TextTestResult),在addFailure和addError里统一处理截图,这样所有用例都能自动生效,不用每个类都写一遍。

日志这边,我用标准logging模块,配置成按天切分文件,同时输出到控制台。关键操作(点击、输入、跳转、断言失败)都打一条 INFO 级别的日志。排查线上环境偶发失败时,日志加截图的组合基本能定位 90% 的问题。

5. 测试报告与执行效率优化

用例能跑通只是第一步,能跑得快、出了问题能一眼看清,才算真正可用。

5.1 报告生成与失败信息可读性

HTMLTestRunner 是比较经典的选择,虽然老但够用。如果希望报告好看一点,BeautifulReport 会更现代,支持截图直接嵌进报告里,这对定位失败用例帮助极大。

封装统一入口run.py:

import unittest, time from common.driver_factory import create_driver from testcases.test_login import TestLogin from testcases.test_order import TestOrder if __name__ == "__main__": suite = unittest.TestSuite() loader = unittest.TestLoader() suite.addTests(loader.loadTestsFromTestCase(TestLogin)) suite.addTests(loader.loadTestsFromTestCase(TestOrder)) ts = time.strftime("%Y%m%d_%H%M%S") with open(f"reports/report_{ts}.html", "wb") as f: runner = unittest.TextTestRunner(stream=f, verbosity=2) runner.run(suite)

用loadTestsFromTestCase而不是手写suite.addTest(TestLogin("test_xxx")),是因为后者每加一个用例都要改代码,前者新增用例自动纳入,维护成本为零。整个testcases目录批量加载可以用discover:

suite = unittest.defaultTestLoader.discover("testcases", pattern="test_*.py")

这条命令的前提是所有用例文件名都以test_开头、类名以Test开头、方法名以test_开头,这三个约定一旦破坏,用例就会静默不执行,排查起来很耗时。我一般会在跑完后核对一下用例总数,发现数量不对就检查命名。

5.2 执行效率与并发取舍

用例数上来之后,串行执行会越来越慢。可选方案有两条路:一是用多进程拆分测试类,二是用 Selenium Grid 分布式执行。

中小项目我更倾向第一种,简单直接:把互相独立的测试类分到不同进程,每个进程跑自己的一份 driver。要注意的是共享资源必须隔离——如果多个进程用同一个测试账号登录,服务端可能会因为并发登录把前一个会话踢掉,导致大量莫名其妙失败。解决办法是给每个进程分配不同账号,或者用不同租户的数据。

另一条提速路径是减少不必要的页面加载。比如某些用例只需要验证接口返回的数据是否正确渲染,可以直接调接口拿数据,只在需要验证 UI 交互时开浏览器。UI 自动化只做 UI 该做的事,纯数据校验交给接口层,这个分工能让套件时间砍掉一大半。

5.3 稳定性治理的几条实操心法

自动化测试最大的敌人不是写不出来,而是偶发失败。一旦出现"重跑就过",团队对这套东西的信任度会迅速崩掉。我踩过不少,总结几条实用的:

  • 绝对不用固定等待。所有等待都要有明确条件,宁可多写一个等待条件,也不要放一个sleep。
  • 测试数据要自己造,不要依赖存量数据。依赖环境里已有的数据,一旦被人删掉或改动,用例立刻失败。要么在setUp里创建,要么用固定的独立测试租户。
  • 每个用例的终态要可预测。有副作用(下单、删除、提交)的用例,要么加清理逻辑,要么用专门的测试环境。
  • 善用重试机制但要谨慎。我一般只对"网络抖动类"的失败加一次重试,业务逻辑失败绝不重试,否则会把真实 bug 掩盖掉。
  • 失败信息要自解释。断言消息里带上实际值、期望值和上下文,比如self.assertEqual(count, 3, f"搜索结果数量不符,实际 {count}"),比光秃秃的assertEqual好用太多。

6. 常见问题排查速查表与高频面试点

这一节把我这些年遇到的高频问题整理成速查表,方便对着症状找原因。

6.1 定位类问题排查

症状常见原因解决思路
NoSuchElementException元素还没渲染出来换成显式等待,检查是否在 iframe 内
元素找到了但点不动被遮挡、不可见、禁用状态先滚动到可见区域,检查遮罩层
StaleElementReferenceException页面刷新后元素引用失效重新定位,不要缓存元素对象
定位到多个元素报错XPath 或选择器不够精确加索引或改用更精确的属性
ElementClickInterceptedException有浮层或动画遮挡等待浮层消失,或用 JS 点击兜底
iframe 内元素找不到没切换框架上下文switch_to.frame()后再定位

StaleElementReferenceException这个坑值得单独说。很多人图省事,在页面类里把元素对象存成属性,比如self.btn = self.find(...),页面一跳转再点这个self.btn就报错。正确做法是只存定位元组,每次操作用定位元组重新查找。这也是为什么我在 BasePage 里所有方法都接收 locator 而不是 element。

关于 iframe,还有个容易忽略的点:切进去之后一定要记得切回来,不然后续所有定位都会失败。我一般用上下文管理器包一层,或者用switch_to.default_content()显式复位。

6.2 环境与驱动类问题排查

症状常见原因解决思路
浏览器一闪而过脚本执行完就退出,没保持检查是否在tearDownClass里 quit 了
启动报驱动版本不匹配驱动与浏览器主版本不一致Selenium 4 用 Selenium Manager 自动管理
无头模式下元素定位失败无头模式窗口尺寸太小显式设置--window-size=1920,1080
截图是黑屏或空白无头模式渲染时序问题截图前加短等待,或用全页截图方案
中文路径报错编码问题路径统一用英文,文件读写指定 utf-8

无头模式那个窗口尺寸问题特别典型。默认无头窗口大概是 800x600,很多响应式页面在这个宽度下会切换成移动端布局,元素位置和结构全变了,定位自然失败。加一句--window-size=1920,1080就能解决,但这问题不看经验很难想到。

6.3 高频面试问答要点

如果是准备自动化测试相关面试,这几个问题出现频率极高,我把自己理解的回答要点也放上。

问:PO 模式和普通脚本的区别是什么?核心区别在于定位信息与操作逻辑分离。普通脚本里定位和操作混在一起,页面一变就要全量改;PO 把定位收敛到页面类,页面变只改一处,用例层承载业务语义,可读性和可维护性都更高。

问:显式等待和隐式等待能混用吗?不建议。两者混用时实际等待时间不可预测,可能出现远超预期的等待。统一用显式等待,隐式等待设为 0。

问:怎么处理非原生下拉框?原生<select>用 Select 类;div+ul+li 结构要点击触发按钮后用 XPath 定位 li 元素,注意用normalize-space处理文本空白,注意选项可能是虚拟滚动动态渲染的。

问:用例偶发失败怎么排查?先看截图和日志确定失败点,再判断是等待不足、数据依赖、并发冲突还是真 bug。等待不足最常见,其次是数据被其他用例污染。定位清楚后再决定是加重试还是修等待逻辑。

问:自动化测试用例应该覆盖多少?我个人的标准是覆盖核心业务流程和历史上出过 bug 的高风险路径,追求全量覆盖性价比极低。UI 自动化跑得慢、维护成本高,把稳定的主流程守住就够了,边缘场景交给接口层和单元测试。

自动化测试这个活,技术门槛其实不算高,真正的难点在于长期维护中的取舍:什么时候该抽象、什么时候该妥协、什么时候该果断把不稳定的用例下线。我见过太多项目死于"用例越写越多、跑一次失败一半、最后没人看报告"。P0 用例跑得稳、报告看得懂、失败能定位,比用例数量堆到几百条有意义得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 5:13:59

SpringBoot+Vue+MySQL毕设项目:从源码到答辩的完整指南

每年的毕业季&#xff0c;都能在技术社区看到一堆挂着“SpringBootVueMySQL”标题的完整源码包&#xff0c;附带数据库脚本、论文和部署文档。作为一个被这种项目“折磨”过、也帮不少人看过代码的老学长&#xff0c;我可以负责任地说&#xff1a;选这个技术栈做毕设&#xff0…

作者头像 李华
网站建设 2026/10/1 5:13:59

风电最大化消纳:热电联产与储热联合优化控制Matlab实现

我去年接了一个调度优化的活&#xff0c;项目标题很长&#xff1a;风电最大化消纳的热电联产机组联合优化控制&#xff08;Matlab代码实现&#xff09;。说白了就是一件事——北方冬天供暖期&#xff0c;热电机组为了供热&#xff0c;夜间电出力压不下去&#xff0c;而风电在凌…

作者头像 李华
网站建设 2026/10/1 5:13:53

GAN增强行人重识别实战:从数据集、指标到训练避坑指南

简介&#xff1a;基于GAN生成对抗网络的行人重识别毕业设计资源包&#xff0c;面向深度学习、计算机视觉方向的高年级本科生和研究生&#xff0c;适合正在开展相关课题或课程设计的读者。该课题聚焦跨摄像头下的行人检索任务&#xff0c;利用生成对抗网络增强行人特征的判别性与…

作者头像 李华
网站建设 2026/10/1 5:13:37

GitHub日榜速报:从榜单数据到有效信息的筛选与判断

1. 日榜速报到底在解决什么问题每天早上打开 GitHub 的 Trending 页面&#xff0c;看到一堆新项目冒出来&#xff0c;但真正值得花时间研究的可能不到十分之一。这就是我坚持做日榜速报的起点——不是简单搬运榜单&#xff0c;而是帮自己&#xff08;顺便帮读者&#xff09;做一…

作者头像 李华
网站建设 2026/10/1 5:13:08

Google Hacking完全指南:高效搜索语法与实战技巧

1. 开篇&#xff1a;别把搜索引擎当百度用了这些年我见过太多人搜索时只会敲几个关键词&#xff0c;然后翻十来页找答案&#xff0c;效率低到让人着急。实际上&#xff0c;Google 的检索能力远比我们日常用到的部分深得多&#xff0c;一套被圈内称作Google Hacking的语法组合&a…

作者头像 李华
网站建设 2026/10/1 5:12:16

软考高项备考经验分享:选择题、案例分析、论文三科全攻略

我参加软考高项前后折腾了两次&#xff0c;第一次倒在论文上&#xff0c;第二次才踩着线过关。回想起来&#xff0c;三门科目——选择题、案例分析、论文——看起来是分开考&#xff0c;实际是一条完整的链路&#xff1a;选择题是知识储备的地基&#xff0c;案例分析是应用能力…

作者头像 李华

关于博客

这是一个专注于编程技术分享的极简博客,旨在为开发者提供高质量的技术文章和教程。

订阅更新

输入您的邮箱,获取最新文章更新。

© 2025 极简编程博客. 保留所有权利.