刚接触自动化测试那会儿,我也以为只要把 Selenium 装好、写几行 click 和 send_keys 就算会了。直到真正用 Python 去跑一个完整的 Web 测试项目,才发现坑比想象中多得多:元素定位不稳定、浏览器驱动版本对不上、等待时间设错了导致用例偶发失败、弹窗和 iframe 切来切去把自己绕晕……这些都是 Selenium 自动化测试里最常见的坎。这篇文章不准备讲那些能“跑通就行”的玩具 Demo,而是从真实项目视角出发,把用 Python + Selenium 做 Web 自动化的思路、定位策略、等待机制、页面对象封装、数据驱动、问题排查完整拆一遍。适合刚开始接触自动化测试、或者写了一段时间但用例总是不稳定的测试开发同学参考。
1. 内容整体设计与思路拆解
1.1 为什么选 Selenium,而不是其他工具
面对 Web 自动化测试,可选方案其实不少。商业工具有 QTP(UFT)、TestComplete,开源的有 Cypress、Playwright、Puppeteer,还有 Selenium。我见过很多团队在选型时直接奔着“最新最火”去,但最后还是会因为浏览器兼容性、语言生态、团队熟悉度这些问题回过头来用 Selenium。
Selenium 的核心优势在于“标准”两个字。它支持的浏览器最多:Chrome、Firefox、Edge、Safari 都有官方驱动支持。更重要的是,Selenium WebDriver 协议已经成了 Web 自动化的事实标准,这意味着你写的 Python 脚本,以后迁到 Java、C# 或者 Ruby,思路和 API 结构几乎不用改变。对大多数业务团队来说,这是个非常关键的隐性好处——不会被某个具体语言绑死。
另外,Python 本身非常适合测试脚本。它不是编译型语言,改完就能跑,调试方便,而且有 pytest、unittest 这样的测试框架加持,断言、用例管理、报告输出都很顺手。Selenium 和 pytest 搭配,算是我个人最推荐的组合,比自带的 unittest 灵活,参数化、插件扩展、失败重跑都做得更完善。
1.2 自动化测试的适用场景与不适用场景
一个经常被问的问题:什么样的项目才值得引入 Selenium 自动化?我的判断标准很简单——看回归频率和手工成本。
如果一个功能模块,你每个迭代都要手工回归一遍,点击路径固定、数据预期明确,那它就是自动化最好的切入点。比如登录、注册、搜索、下单、权限校验这些核心流程。反过来,如果你要测的是一个只在活动期间上线的临时页面,或者界面 UI 还在频繁改版,自动化脚本改起来可能比手工点还慢,那就别急着上。
还有一个我踩过的坑——把视觉样式检查硬塞给 Selenium。Selenium 的核心价值是模拟用户操作,验证业务流程是否正确,而不是像素级检查颜色、间距、字体。真要查视觉回归,应该用 Playwright 的截图对比或者专门的视觉测试工具。工具选错了,整个自动化项目的维护成本会高得吓人。
2. 环境准备与浏览器驱动配置
2.1 安装 Python 库与基础依赖
这一步看起来简单,但很多新手会在版本上出问题。我建议直接用虚拟环境,别全局装。
python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install selenium pytest安装完成后,先验证一下 Selenium 版本和 Python 环境的兼容性。Selenium 4.x 之后,API 结构有过一次比较大的调整,比如用webdriver.Chrome()而不是webdriver.Chrome(executable_path=...)。我在项目里遇到过同事拿老教程里的写法去跑新版 Selenium,直接报TypeError。所以如果你刚开始学,建议直接装最新版,并到官方文档确认当前版本的调用方式。
pip show selenium如果是在公司内网环境,pip 源可能有限制,可以配置国内镜像源。这个属于环境问题,但没处理好,后续安装会卡很久。
2.2 WebDriver 与浏览器版本匹配
这是 Selenium 新手遇到的第一座大山。Chrome 浏览器会自动更新,但你本地的 chromedriver 版本不会跟着变,版本对不上,启动浏览器就会报SessionNotCreatedException。
核心原则是:WebDriver 的大版本号必须与浏览器主版本一致。比如你的 Chrome 是 122,那就得下载 chromedriver 122 的版本。下载地址比较分散,第三方镜像站多,我不建议在博文里粘贴具体链接,因为浏览器版本更新太快,链接会失效。更靠谱的做法是使用 webdriver-manager 这个库,它能自动检测浏览器版本并下载匹配的驱动。
pip install webdriver-managerfrom selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())) driver.get("http://example.com")这个方案在本地和 CI 环境里都能用,省掉了手动升级驱动的痛苦。不过注意,如果公司网络屏蔽了外部下载地址,webdriver-manager 也会失效,那就只能找运维统一维护驱动库了。
2.3 第一个最小用例与浏览器启动参数
写第一个用例不需要太复杂。我建议先做一件事:启动浏览器、访问一个页面、打印标题、退出。
from selenium import webdriver driver = webdriver.Chrome() driver.get("https://example.com/") print(driver.title) driver.quit()这里有个容易忽视的习惯:最后一定要driver.quit(),而不是driver.close()。close()只关闭当前窗口,浏览器进程可能还残留在后台;quit()才会真正释放整个浏览器实例。测试用例多了以后,如果每个用例都残留一个浏览器进程,内存会被慢慢吃光,CI 机器上最终会出现一堆僵尸进程。
还可以加一些启动参数,让浏览器更适合自动化环境运行:
from selenium import webdriver options = webdriver.ChromeOptions() options.add_argument("--headless=new") # 无头模式,适合CI # options.add_argument("--window-size=1920,1080") driver = webdriver.Chrome(options=options)无头模式就是在后台运行浏览器,不显示界面。本地调试时建议关掉,方便看实时操作;CI 上建议打开,节省资源。这个参数在不同版本的 Chrome 里名称有变化,老版本可能用--headless,新版用--headless=new。
3. 核心细节解析与实操要点
3.1 元素定位的优先级:id、CSS、XPath 怎么选
Selenium 定位元素的手段很多,包括id、name、class name、tag name、link text、partial link text、xpath、css selector。我实际项目里的定位优先级是:id > CSS Selector > XPath > 其他。
原因很简单:id 在一个正常页面里应该是唯一的,定位速度最快,代码可读性也最高。找不到 id 的时候,优先考虑 CSS Selector,因为它的语法简洁,性能和稳定性都比 XPath 好。XPath 虽然功能强大,但容易写出很长很脆弱的绝对路径,元素结构稍微变一点就崩。
举个例子,一个搜索框,HTML 可能长这样:
<div class="search-wrap"> <input id="kw" class="search-input" name="keyword" /> </div>定位方式分别可以写:
from selenium.webdriver.common.by import By driver.find_element(By.ID, "kw") driver.find_element(By.CSS_SELECTOR, "input.search-input") driver.find_element(By.CSS_SELECTOR, ".search-wrap > input[name='keyword']") driver.find_element(By.XPATH, "//div[@class='search-wrap']/input")我的经验是,只要 id 存在,就优先用 id。如果 id 是动态生成的(比如每刷新一次就变一次),那就想办法找稳定的父节点,用相对路径去定位。千万不要复制浏览器 DevTools 里自动生成的 absolute xpath,那种路径长不拉几,而且一旦页面多包了一层 div 就全废了。
还有一个实用技巧:用find_elements(复数)先判断元素数量,再决定操作。比如确认某个弹窗是否出现,可以这样写:
elements = driver.find_elements(By.CSS_SELECTOR, ".modal-title") if elements: print("弹窗出现") else: print("弹窗未出现")这种方式比直接用find_element然后捕获异常要优雅得多。
3.2 等待机制:隐式等待、显式等待与强制等待
很多测试脚本不稳定,八成是等待没写对。Selenium 执行操作时,页面元素未必已经渲染完成,立刻去点击,就会报NoSuchElementException或ElementClickInterceptedException。
最粗暴的办法是time.sleep(2),也就是强制等待固定时长。我承认,初学时谁都用过,但它有三个问题:第一,浪费执行时间,明明 0.5 秒就能加载完,你非要等 2 秒;第二,环境波动时不稳定,网络慢的时候 2 秒不够,网络快的时候 2 秒又多余;第三,代码里密密麻麻的 sleep 会让维护者抓狂。所以能不用尽量不用。
隐式等待是给 driver 全局设置一个最长等待时间,只要查找元素时没找到,就在这个时间内轮询等待。
driver.implicitly_wait(10)隐式等待的粒度是“查找元素”,它不保证元素可交互。比如一个按钮,HTML 已经存在但被 disabled 属性锁住了,隐式等待照样能找到它,点击时却会失败。而且隐式等待设置一次对整个 driver 生命周期都生效,某些特殊场景反而会拖慢其他操作的等待时间。
显式等待是更精细的做法,针对某个特定条件进行等待,也是我在项目里最推荐的方式。
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 10) login_button = wait.until(EC.element_to_be_clickable((By.ID, "login-btn"))) login_button.click()这里有几个常用的条件:
visibility_of_element_located:元素可见且非空presence_of_element_located:元素存在于 DOM 中,不判断可见element_to_be_clickable:元素可见且可点击text_to_be_present_in_element:元素文本包含指定内容alert_is_present:等待弹窗出现
我一般给每个页面的关键元素封装等待函数,而不是每处都裸写WebDriverWait。下面是一个简化示例:
def wait_clickable(driver, locator, timeout=10): return WebDriverWait(driver, timeout).until( EC.element_to_be_clickable(locator) )接产品需求的习惯:页面加载的“标准时间”很难固定,所以显式等待的超时时间我习惯统一设 10 秒,个别慢接口再加到 20 秒。这么设计的好处是,跑挂了能在日志里快速看出是哪个元素超时。
3.3 表单操作、下拉框与文件上传的常见写法
表单操作是 Web 自动化里最常用的场景。常规输入框,直接send_keys就行,但有几个细节需要注意。
先清空再输入。如果输入框有默认值,直接用send_keys会追加,而不是替换。所以稳妥写法是:
input_box = driver.find_element(By.ID, "username") input_box.clear() input_box.send_keys("test_user")如果遇到readonly属性导致的无法输入,可以先用 JavaScript 去掉 readonly:
driver.execute_script("document.getElementById('username').removeAttribute('readonly')")这招在测试一些日期选择组件时特别有用。很多日期控件并不是原生 input,而是弹出一个日历面板,手工点起来累得要死。直接给 input 赋值发送回车,往往就绕过去了。
下拉框如果用的是原生<select>标签,推荐用Select类:
from selenium.webdriver.support.ui import Select select = Select(driver.find_element(By.ID, "city")) select.select_by_visible_text("杭州") select.select_by_value("hangzhou") select.select_by_index(2)如果下拉框不是原生 select,而是自定义的 div 列表,那只能老老实实展开选项再点击。可以先点击下拉框容器,然后用文本匹配所有候选元素,找到目标项再 click。
文件上传是比较容易踩坑的点。input 标签类型为 file 时,直接send_keys传本地路径即可,不需要模拟 Windows 文件选择框:
upload_input = driver.find_element(By.CSS_SELECTOR, "input[type='file']") upload_input.send_keys("/path/to/your/file.pdf")但如果上传是自定义组件,点击后弹出系统文件选择窗口,Selenium 是不能直接操作系统弹窗的。这时可以用pyautogui之类的外部工具,也可以让开发配合把input[type=file]暴露出来。我在实际项目里遇到的自定义上传组件基本都是找人改成一个隐藏的 file input,这样更稳定。
3.4 断言设计与测试报告
自动化测试如果没有断言,脚本跑完也等于白跑。断言不是简单判断“页面有没有报错”,而是根据业务规则验证关键节点是否符合预期。
pytest 原生的断言用起来最简单:
def test_login_success(): driver = webdriver.Chrome() driver.get("http://example.com/login") driver.find_element(By.ID, "username").send_keys("user01") driver.find_element(By.ID, "password").send_keys("pass123") driver.find_element(By.ID, "submit").click() assert driver.current_url == "http://example.com/dashboard" assert "欢迎回来" in driver.page_source driver.quit()这不仅判断了跳转地址,还判断了页面内容。如果登录失败,通常会停留在登录页或者有错误提示,这两个断言就会挂掉。
对于更复杂的业务,可以把断言拆成“状态断言”和“数据断言”。状态断言判断操作是否产生了预期的状态变化,比如弹窗出现、按钮变成禁用;数据断言则检查页面展示的数据是否和预期一致,比如订单号、金额、列表条数。我比较喜欢把测试数据单独放一个 JSON 或 YAML 文件,然后在测试代码里读取,这样业务人员也可以维护用例数据。
测试报告方面,pytest 配pytest-html是入门首选:
pip install pytest-html pytest --html=report.html生成的 HTML 报告会展示每个用例的执行时间、失败截图、日志。配合allure-pytest能做更漂亮的报告,但对新手有点重。我现在的习惯是先用 pytest-html 撑起基本报告,后续有需求再迁移 Allure,避免一上来就被复杂配置劝退。
4. 实操全过程:一个完整的登录流程测试
4.1 项目目录结构与测试用例规划
为了让你有一个可以直接照着做的范例,我拆一个模拟项目 X 的登录功能。目录结构可以这样设计:
project/ ├── pages/ │ ├── __init__.py │ └── login_page.py ├── tests/ │ ├── __init__.py │ ├── conftest.py │ └── test_login.py ├── data/ │ └── login_data.json ├── reports/ │ └── (HTML报告输出) └── requirements.txt这个结构把页面对象、测试用例、测试数据分开,后续维护时不需要在一堆代码里翻找。我想强调的是,测试代码的可维护性比测试代码本身更重要。业务改版后,你只需要去pages里改定位,而不用每个 test 函数里都动刀。
登录流程的用例规划,至少要覆盖这几条:
- 正确账号密码登录成功,跳转到首页
- 用户名错误,提示登录失败
- 密码错误,提示登录失败
- 用户名为空,前端拦截
- 密码为空,前端拦截
每一条独立成一个 test 函数,互不依赖。因为用例一旦互相依赖,前面的失败会让后面的全部失败,排查问题时会非常头疼。
4.2 用页面对象模式封装登录页
页面对象模式(POM)的核心思想:把每个页面的定位器和操作逻辑封装成一个类,测试脚本只调用页面对象提供的方法。
# pages/login_page.py from selenium.webdriver.common.by import By class LoginPage: def __init__(self, driver): self.driver = driver self.username_input = (By.ID, "username") self.password_input = (By.ID, "password") self.submit_button = (By.ID, "submit") self.error_tip = (By.CSS_SELECTOR, ".error-tip") def open(self, url): self.driver.get(url) def input_username(self, username): self.driver.find_element(*self.username_input).send_keys(username) def input_password(self, password): self.driver.find_element(*self.password_input).send_keys(password) def click_submit(self): self.driver.find_element(*self.submit_button).click() def get_error_tip(self): return self.driver.find_element(*self.error_tip).text def login(self, username, password): self.input_username(username) self.input_password(password) self.click_submit()这里的*self.username_input是把元组展开成By.ID, "username"传入find_element。用元组保存定位器有个好处:后续如果想改成显式等待,直接将元组传给WebDriverWait即可,不需要改定位器定义。
测试用例里调用就非常干净:
# tests/test_login.py from pages.login_page import LoginPage这就是 POM 最大的价值:业务操作和测试脚本解耦。登录按钮换了 id,只需要改login_page.py,测试代码一行都不用动。
4.3 用 pytest 框架组织用例与参数化
pytest 的setup_method或 fixture 可以很好地完成浏览器初始化和回收。我在conftest.py里定义一个 fixture,每个用例自动启用浏览器。
# tests/conftest.py import pytest from selenium import webdriver @pytest.fixture def driver(): options = webdriver.ChromeOptions() options.add_argument("--window-size=1920,1080") driver = webdriver.Chrome(options=options) driver.implicitly_wait(5) yield driver driver.quit()测试用例可以写成:
# tests/test_login.py from pages.login_page import LoginPage def test_login_success(driver): login_page = LoginPage(driver) login_page.open("http://example.com/login") login_page.login("user01", "pass123") assert "dashboard" in driver.current_url多组数据时,用 pytest 的参数化非常方便:
import pytest @pytest.mark.parametrize("username,password", [ ("user01", "wrong_pass"), ("wrong_user", "pass123"), ("", "") ]) def test_login_failed(driver, username, password): login_page = LoginPage(driver) login_page.open("http://example.com/login") login_page.login(username, password) error_text = login_page.get_error_tip() assert "用户名或密码错误" in error_text参数化让用例表驱动,数据和代码分开,后续要加测试组合,只需要往列表里新增元组。有些团队会把数据放到 JSON 文件,然后用json.load读取,这个我建议根据团队习惯来,数据量小就直接写在装饰器里更直观。
4.4 失败截图与执行调试
用例跑挂时,如果没有任何现场信息,排查会非常痛苦。我习惯在 pytest 的 fixture 里做一个失败截图钩子,一旦某个用例异常,自动把当前浏览器画面截图保存下来。
import os import pytest from selenium import webdriver @pytest.fixture def driver(): options = webdriver.ChromeOptions() driver = webdriver.Chrome(options=options) driver.implicitly_wait(5) yield driver if pytest.exitstatus and hasattr(driver, "get_screenshot_as_file"): pass driver.quit()不过上面这段比较粗糙,更正规的做法是用 pytest 的 hook,在pytest_runtest_makereport里判断failed,然后driver.get_screenshot_as_png()。但这会涉及到 fixture 作用域的问题,所以我更推荐直接用 pytest-selenium 或 pytest-playwright 里的现成方案。最省事的框架是安装pytest-html后,配合--html=report.html --self-contained-html,它会在报告里展示失败时的堆栈,截图则需要自己写。
我最终用的是这样的简单版本:
import pytest from pathlib import Path @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: driver = item.funcargs.get("driver") if driver: screenshot_dir = Path("reports/screenshots") screenshot_dir.mkdir(parents=True, exist_ok=True) driver.save_screenshot(str(screenshot_dir / f"{item.name}.png"))这段代码的作用是,在用例失败时从 fixture 参数里取出 driver,然后截图到reports/screenshots目录。不要小看这个功能,很多难复现的偶发问题,就是靠失败现场截图和 HTML 报告里的 log 一起定位出来的。
5. 常见问题与排查技巧实录
5.1 元素找不到与超时的排查路径
NoSuchElementException可以说是 Selenium 自动化里最高频的报错。报错本身不可怕,可怕的是你对着代码看半天也不知道为什么。我排查时有一套固定顺序:
第一,检查选择器是否写错。先在浏览器 DevTools 的 Console 里用document.querySelectorAll验证一下选择器能不能找到元素。第二,检查元素是否在 iframe 里。这个很坑,元素在 iframe 内时,主文档里永远找不到,必须先switch_to.frame。第三,检查页面是否真的加载到了预期状态。可能是点击后页面跳转还没完成,元素还没渲染,需要显式等待。第四,检查是否存在多个同名的元素。find_element默认返回第一个,如果页面里有两个相同 id(这属于开发不规范),你找到的可能是隐藏的那个。
我遇到过一个最隐蔽的案例:页面上的按钮在鼠标悬停时才出现,直接find_element永远找不到。后来换成先ActionChains(driver).move_to_element(parent_node).perform(),把鼠标移到父容器上,按钮才出现在 DOM 里。
5.2 iframe 与弹窗处理
iframe 是 Web 自动化里最烦人的结构之一。页面里嵌了广告、第三方支付、地图组件,都会导致主文档里找不到目标元素。
处理 iframe 的基本步骤是:先定位 iframe 元素,再切换进去,操作完再切回主文档。
from selenium.webdriver.common.by import By # 切换到 iframe(用 name 或 id 或元素) driver.switch_to.frame("iframe_name") # 或者 driver.switch_to.frame(driver.find_element(By.CSS_SELECTOR, "iframe[src='xxx']")) # 操作 iframe 内部元素 # ... # 切回主文档 driver.switch_to.default_content()如果 iframe 是嵌套的,那就要一层一层切进去,操作完再一层一层退回来。这里我建议在操作完 iframe 后一定记得切回default_content(),否则后续主文档的操作全部会失败。
弹窗分为两种:浏览器原生 alert 和页面自定义弹窗。原生 alert 用 Selenium 处理很简单:
alert = driver.switch_to.alert alert.accept() # 确定 alert.dismiss() # 取消 alert.send_keys("some text")但页面自定义弹窗(div 实现的模态框)并不是真正的 alert,它只是一个常规元素,直接点击关闭按钮就好。如果不能确定是哪一种,可以在测试里先尝试switch_to.alert,如果抛异常,再回到元素定位流程。
5.3 浏览器窗口、多标签页切换
有时候点击target="_blank"链接会打开新标签页,但 driver 还停留在旧页面。必须切换到新标签页才能继续操作。
比较原始的做法是:
# 记录当前窗口句柄 current_handle = driver.current_window_handle # 执行点击操作 driver.find_element(By.ID, "open-new-tab").click() # 等待新标签页出现 wait.until(EC.number_of_windows_to_be(2)) # 切换到新标签页 new_handle = [handle for handle in driver.window_handles if handle != current_handle][0] driver.switch_to.window(new_handle)这段代码不复杂,但每次写都挺烦。我习惯封装成工具函数:
def switch_to_new_window(driver): handles_before = set(driver.window_handles) WebDriverWait(driver, 10).until( lambda d: len(d.window_handles) > len(handles_before) ) new_handle = [h for h in driver.window_handles if h not in handles_before][0] driver.switch_to.window(new_handle)窗口切换时还容易踩的一个坑是:新窗口没加载完就去取元素。所以切换后先等一个关键元素出现,再执行后续操作。
5.4 提升用例稳定性:避免被检测与调度干扰
有些网站会针对自动化脚本做检测,比如通过navigator.webdriver属性判断浏览器是否被自动化控制。如果你的测试目标是公司内部系统,通常不会遇到这种问题,但如果是测试外部网站,就可能会被识别。
规避手段有很多,但我要提醒一点:不要拿着这套方法去干扰正常业务、规避平台规则。我这里讲只限于合法的内部测试环境。一个比较常见的思路是加启动参数隐藏自动化特征:
options = webdriver.ChromeOptions() options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False)不过这些参数在不同 Chrome 版本里效果不一样,不能百分百保证不被识别。更稳定的做法是控制操作节奏,模拟人的行为习惯。比如输入时不一次性 send_keys,而是逐字输入并加一点随机延时;点击前把鼠标先移动到目标位置。
稳定性提升的另一条路径是“不要过度依赖等待”,而是“主动确认”。点击一个按钮后,不要马上点下一个,而是等待某个标志性元素出现。我在前面讲显式等待时其实已经提到了,这里再说得更直接一点:测试脚本里最怕看到一长串点击操作,没有任何中间校验。一旦某一步失败,后面的操作全跟着崩。正确做法是每完成一个关键动作,就等待对应的状态变化,这样的脚本才具备自我纠错能力。
5.5 快速问题排查表
| 报错或现象 | 大概率原因 | 解决方向 |
|---|---|---|
NoSuchElementException | 选择器错误 / iframe 未切换 / 元素未加载 | 先用 DevTools 验证选择器,再看是否在 iframe,最后加显式等待 |
ElementClickInterceptedException | 元素被遮挡 / 页面动画未结束 | 等待元素可点击,或用 JS 点击driver.execute_script("arguments[0].click()", el) |
StaleElementReferenceException | 页面刷新导致元素引用失效 | 重新查找元素,不要在循环里持有旧的元素引用 |
SessionNotCreatedException | WebDriver 与浏览器版本不匹配 | 升级/降级驱动,或使用 webdriver-manager |
TimeoutException | 页面加载超长 / 条件选择错误 | 检查网络,确认等待的条件是否达成 |
| 浏览器进程残留 | 用了close()而非quit(),或异常中断 | 在finally或 fixture teardown 中调用quit() |
| 偶发失败,重跑又过 | 等待不足 / 数据污染 / 外部接口抖动 | 加显式等待,检查用例之间是否有共享数据,必要时失败重跑 |
这张表是我平时排障时经常翻的速查表。如果你的用例出现了表格之外的诡异问题,我建议立刻打开浏览器日志和控制台输出,先排查网络请求和前端报错,再回头看测试代码。很多自动化问题,根源其实不在 Selenium,而在页面本身的稳定性。
我自己的一个习惯是,给每个测试项目初始化一套“失败重跑”策略,pytest 里用pytest-rerunfailures插件,对偶发失败先重跑两次,避免干扰真实 bug 的判断。
pip install pytest-rerunfailures pytest --reruns 2 --reruns-delay 1不过要严格控制重跑次数,一般不超过两次。如果重跑后还是失败,那基本就是真的问题,需要认真排查了。Selenium 自动化测试做到最后,拼的不是你会写多少种定位方式,而是你能不能把脚本的稳定性打磨到“可以交给 CI 定时跑”的程度。这个目标说难不难,说简单也不简单,把等待、定位、iframe、截图这些问题一个个梳理清楚,路就顺了。