尧图精选

Python+Selenium Web自动化测试实战:从环境搭建到工程化落地

🕒 发布时间:2026/10/2 15:29:56 📁 来源:尧图网络
1. 项目目标与整体设计思路1.1 这个自动化项目到底想解决什么问题先聊聊我为什么要做这个项目。日常工作里Web端的回归测试最让人头疼早上改了一个搜索逻辑下午就得把登录、搜索、筛选、分页、下单这一条链路全部手工点一遍。点一遍也就罢了问题是下个星期又有人改样式、改接口同样的流程又要重新点。这种重复劳动消耗的不仅是时间还有测试人员的耐心。我当时的想法很简单——能不能让Python脚本替我把这些固定流程全部跑掉跑完自动生成一份结果文件有问题直接定位到具体步骤于是就有了这个用Python Selenium实现的Web自动化测试项目。它的核心目标有三个第一把高频回归用例从手工点击变成脚本执行缩短回归周期第二通过断言和等待机制代替人眼判断减少漏测第三积累一套可复用的页面操作层后续新功能上线时能快速拼装测试场景。如果你也在做Web测试、维护一个长期迭代的业务系统或者想转测试开发方向这套东西都可以作为参考起点。1.2 为什么选Python Selenium这套组合技术选型的时候我也对比过不少方案。商业自动化工具确实省事但License费用高而且脚本灵活性不够Java Selenium成熟但写起来啰嗦环境配置也重最后我选了Python Selenium理由很实际Python语法直观团队里做测试的同事上手成本低Selenium对主流浏览器的支持又足够全面。其实现在很多团队还在纠结“要不要上自动化测试工具”我觉得工具不是重点重点是你有没有理解自动化测试解决的是“重复验证”场景。如果你的业务一周只发一次版回归范围小手工点一遍反而比维护一套脚本更划算。但如果你的系统每天都有迭代、需要频繁验证主流程那Selenium这套自动化方案投入产出比非常高。为什么不是Playwright或者Puppeteer这里要说明一下。我做这个项目的时间比较早当时Selenium在文档、社区、人才储备上都更成熟。现在如果你的项目是全新项目我也建议你了解一下Playwright它在等待机制和自动截图方面确实更省心。但Selenium依然有不可替代的场景老项目维护、跨浏览器兼容性测试、企业内部大量历史用例的迁移Selenium WebDriver协议已经成了事实标准很多云测试平台后端跑的还是这套协议。1.3 项目结构与模块划分方案项目开始前我就把脚本结构规划好了这一点很重要。很多人写自动化脚本从一开始就没规划所有代码堆在几个长函数里最后变成一团乱麻。我的目录结构大致如下web_auto_test/ ├── config/ │ ├── __init__.py │ └── settings.py # 环境配置、URL、账号信息 ├── pages/ │ ├── __init__.py │ ├── base_page.py # 页面基础类封装公共方法 │ ├── login_page.py # 登录页面对象 │ └── search_page.py # 搜索页面对象 ├── test_cases/ │ ├── test_login.py # 登录测试用例 │ ├── test_search.py # 搜索测试用例 │ └── conftest.py # pytest fixtures负责浏览器启动和收尾 ├── utils/ │ ├── screenshot.py # 截图工具 │ └── logger.py # 日志记录 ├── reports/ # 测试报告和截图输出 └── requirements.txt模块划分的原则是“三层分离”页面操作层只管元素定位和操作动作测试用例层只负责场景编排和数据断言配置数据从代码里拆出去单独管理。这样当页面改版时最坏情况只需要改Page层用例层和数据层不用动当测试数据变了改一下配置文件或者用数据驱动的方式加载即可代码保持不变。1.4 项目启动前的几个取舍做这个项目之前我还认真考虑过几个问题也想分享给你是走“录制回放”还是“纯手写脚本”录制回放工具比如老版的Selenium IDE看起来方便但生成的脚本冗余严重、定位策略不稳定稍微改版就挂。纯手写脚本虽然前期慢一点但每一步都在自己掌控中后续维护省心太多。测试数据放在哪一句话——不要硬编码在用例里。账号密码、URL、测试关键词这类数据一旦写死在代码中换环境就要改代码很容易出错。我统一放到配置文件里通过读取配置传入用例切环境时只改配置。要不要一开始就上数据驱动如果你的项目只有三五个用例不必强行框架化。但我这个项目因为后面要覆盖多个业务模块所以我从开始就预留了数据驱动的接口用pytest的参数化功能把“测试数据”和“用例逻辑”解耦开。这一点在后来的扩展中帮了大忙。2. 环境准备与Selenium安装2.1 Python环境搭建与虚拟环境隔离这个项目依赖Python 3我强烈建议你在开始之前先建一个虚拟环境不要直接往系统Python里装依赖。为什么因为你的机器上大概率还有其他项目每个项目依赖的库版本可能不一样。比如项目A用Selenium 3项目B用Selenium 4接口和部分API已经不兼容了如果你把它们混在同一个环境里早晚出事。# 创建项目目录并进入 mkdir web_auto_test cd web_auto_test # 创建虚拟环境 python -m venv venv # 激活虚拟环境Windows venv\Scripts\activate # 激活虚拟环境macOS / Linux source venv/bin/activate进入虚拟环境后命令行前面会出现(venv)标记看到这个标记就说明你已经在独立环境里面了。这个操作看起来多花了一分钟但能避免后面几十个小时的依赖冲突困扰非常值得。关于Python版本建议3.8以上。Selenium 4.6已经内置了驱动自动管理功能对Python版本有一定要求3.8以下容易踩到语法兼容的坑。如果你是Windows用户安装Python时一定要勾选“Add Python to PATH”这个选项不勾的话后面在命令行里敲python会提示找不到命令非常扫兴。2.2 安装Selenium库与验证环境在虚拟环境激活的状态下安装Selenium只需要一条命令pip install selenium如果你想固定版本防止未来升级破坏兼容性可以指定版本号安装pip install selenium4.21.0装完之后我习惯立即验证一下环境是否OK而不是直接开始写代码。验证方式很简单跑一个最小脚本让浏览器打开一个空白页面from selenium import webdriver driver webdriver.Chrome() driver.get(about:blank) print(driver.title) driver.quit()如果能正常弹出一个Chrome浏览器窗口并打印空白页之类的标题说明环境通畅。如果这一步就报错九成是浏览器驱动的问题往下看。2.3 浏览器驱动下载与自动管理Selenium需要配合对应的浏览器驱动driver才能工作这是新手最容易栽跟头的环节。Chrome浏览器需要用ChromeDriverEdge需要用EdgeDriverFirefox需要用GeckoDriver版本还必须和浏览器主版本号匹配。在Selenium 4.6以前你需要手动去下载驱动、解压、放到PATH目录里还要手动维护版本号浏览器一升级测试就崩极其痛苦。现在Selenium 4.6内置了Selenium Manager它会在第一次启动时自动检测浏览器版本、自动下载匹配的驱动。所以如果你用的是最新版Selenium其实什么都不用配。我自己在项目里仍然是手动指定驱动路径的因为公司环境里浏览器版本是统一锁定的自动下载拿到的驱动可能不符合IT部门的软件管理要求。这种情况下可以手动下载驱动并指定路径from selenium import webdriver from selenium.webdriver.chrome.service import Service service Service(/path/to/chromedriver) driver webdriver.Chrome(serviceservice)这里有个注意点Chrome浏览器和Chromedriver的版本号前三位要一致比如浏览器是120.0.6099那么驱动也得是120.0.6099系列。如果主版本不一致启动时会报“session not created”或“This version of ChromeDriver only supports Chrome version XX”之类的错误。解决办法就是去对应站点下载匹配版本别下错。2.4 初装环境时最容易踩的三个坑先说第一个坑pip install装好了Selenium运行脚本却报ModuleNotFoundError: No module named selenium。大概率是你的pip装到了系统Python而你运行脚本时用的是虚拟环境Python或者反过来。排查方法是在命令行里分别执行which python和pip list确认是不是同一个环境的路径不要光看报错就在那里瞎猜。第二个坑执行脚本后浏览器窗口一闪而过。这通常是因为脚本执行完了浏览器来不及加载页面就退出了。排查的时候可以先把driver.quit()注释掉让浏览器保持打开状态看看是不是页面加载逻辑有异常。另外Selenium在脚本退出时默认会关闭浏览器如果遇到闪退优先检查quit和get之间的代码有没有异常抛出。第三个坑公司网络限制下载驱动或者自动下载慢到怀疑人生。解决思路是让IT部门统一提供驱动包或者你在本地下载好之后把驱动放在固定目录通过Service参数指定路径从源头上避开自动下载依赖网络的问题。这个操作我在项目里一直沿用既能保证版本可控也方便团队成员通过共享目录共用同一个驱动版本。3. Selenium底层机制与页面等待策略3.1 浏览器驱动到底在做什么刚开始用Selenium的时候我一直把它当成一个“模拟鼠标点击的库”后来踩过几次不稳定性的坑才意识到必须理解它的底层机制。Selenium的架构是Python代码通过WebDriver协议向浏览器驱动发送命令驱动再把命令翻译成浏览器能理解的原生操作控制真实的浏览器页面。这个“翻译”过程不是瞬时的浏览器加载页面、渲染DOM、执行JavaScript都需要时间。这就引出了自动化测试里最核心也最容易被忽略的概念——等待策略。如果你一边看代码一边手工操作你会有意无意地在步骤之间留出反应时间而脚本不同它发出指令后立刻就会问下一步“元素出来了吗”。如果元素还没渲染出来脚本就会去找一个不存在的节点然后抛异常。所以自动化测试的稳定性很大程度取决于你如何处理“页面加载和元素查找”之间的时序。3.2 三种等待方式的适用场景Selenium里有三套等待手段很多人混着用结果越用越乱。我分别说清楚强制等待也就是time.sleep(3)。它的逻辑是“不管页面什么状态老子就是等3秒”。优点是简单缺点是极其浪费时间和不稳定。页面1秒就加载完你得白等2秒页面10秒才加载完你等3秒照样找不到元素。除非是在调试阶段否则我不建议你在正式用例里大量使用强制等待。隐式等待通过driver.implicitly_wait(10)设置。它的逻辑是在查找元素时如果DOM里没有立即出现目标元素WebDriver会在一定时间内持续轮询查找直到超时。设置一次对后续所有元素的查找都生效。注意隐式等待只作用于find_element不作用于元素的状态判断比如元素是否可点击、是否可见它管不了。显式等待通过WebDriverWait配合expected_conditions实现。它的逻辑是等待某个元素满足指定条件比如可见、可点击、包含某段文字满足则立即继续超时则抛异常。三者的关系我打个比方隐式等待是“在餐厅等菜服务员隔一会儿就去后厨看一眼”显式等待是“等一位指定的朋友到店他一进门你立刻就能看到”而强制等待就是“不看表干坐着反正30分钟以后再说”。优化后的用法是全局用隐式等待兜底关键操作节点用显式等待控制精确时机代码里几乎不用sleep。3.3 显式等待怎么写才规范显式等待的写法有很多我提供一个我自己项目里重复使用的基础模板from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 等待某个元素可见超时10秒 wait WebDriverWait(driver, 10) wait.until( EC.visibility_of_element_located((By.ID, login-button)) )这里有个非常容易踩的坑visibility_of_element_located接收的参数是一个元组(定位方式, 定位表达式)不是两个独立参数。很多人写成EC.visibility_of_element_located(By.ID, login-button)直接报错。这个括号问题看起来小但几乎每个初学者都会遇到定位报错但又不明白语法哪里错了。常用等待条件我整理一下等待条件作用使用场景presence_of_element_located元素出现在DOM页面刚跳转只需要确认元素存在visibility_of_element_located元素可见元素渲染出来且非隐藏最常见element_to_be_clickable元素可见且可点击按钮最终可点了再执行点击text_to_be_present_in_element元素包含指定文本等待加载结果、列表刷新alert_is_present弹窗出现处理alert弹窗我自己的习惯是凡是点击类的元素一律用element_to_be_clickable因为仅仅“存在”或“可见”还不够有时候元素在DOM里但是被遮罩盖住或者宽度为0硬点会提示“element not interactable”。用可点击条件就能把这类问题在前置阻塞掉。3.4 自己封装一个稳健的等待工具写了半年用例之后我发现原生的显式等待还是有两个问题第一每次都要写WebDriverWait(driver, 10)模板代码重复性高第二超时异常信息不友好只说“Timed out waiting for element”不说具体是哪个元素。所以我后来封装了一个简单的工具方法from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By def wait_for_element(driver, locator, timeout10): locator为元组例(By.ID, login-button) try: return WebDriverWait(driver, timeout).until( EC.visibility_of_element_located(locator) ) except Exception as e: raise AssertionError( f等待元素超时: {locator}, {timeout}s, 原始异常: {e} ) # 使用方式 login_btn wait_for_element(driver, (By.ID, login-button), timeout15)这样设计的好处是一是统一了等待条件项目里不会出现一个地方用可点击、另一个地方用可见逻辑混乱二是异常信息里带了具体的定位表达式报错后直接复制表达式去开发者工具里查排查效率提升很大。说到排查后面第7章我会详细写一个“元素找不到”的标准排查路径这里先不展开。4. 元素定位实战与用例开发4.1 八种定位方式到底怎么选Selenium提供了八种定位方式id、name、class_name、tag_name、link_text、partial_link_text、xpath、css_selector。新手最容易犯的错是只用XPath而且一上来就是//div/div[1]/div[2]/span这种从根节点一路写下来的绝对路径稍微加个节点就崩。我的选择优先级是这样的有id用idid是页面里最稳定的标识一个页面里理论上唯一没有id就用name或class_name前提是保证唯一链接类元素用link_text更方便复杂结构才用css_selectorxpath只在没有好用的选择器、需要依赖文本内容或层级关系时才用。举一个实际例子下面几个定位表达式效果一样但稳健性和可维护性完全不同# 推荐通过id定位 driver.find_element(By.ID, username) # 次选通过name定位 driver.find_element(By.NAME, username) # 普通通过css选择器 driver.find_element(By.CSS_SELECTOR, input[nameusername]) # 慎用绝对路径xpath driver.find_element(By.XPATH, /html/body/div[1]/form/input[1])你可以打开Chrome的开发者工具在Elements面板里对着目标元素点右键直接就拷出copy selector或者copy xpath但要注意复制出来的XPath往往是绝对路径一改版就失效。我更推荐自己写相对XPath比如//input[idusername]这种比绝对路径稳定得多。4.2 写一个完整的登录用例光说不练没用我直接放一个真实的登录用例包含等待和断言你可以直接抄着改成自己的业务场景import pytest from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from config.settings import BASE_URL, USERNAME, PASSWORD pytest.fixture(scopefunction) def driver(): drv webdriver.Chrome() drv.maximize_window() drv.implicitly_wait(5) yield drv drv.quit() def test_login_success(driver): driver.get(BASE_URL /login) # 显式等待登录按钮出现 WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, submit-btn)) ) # 输入账号密码 driver.find_element(By.ID, username).send_keys(USERNAME) driver.find_element(By.ID, password).send_keys(PASSWORD) driver.find_element(By.ID, submit-btn).click() # 断言跳转到了主页并且右上角显示用户名 WebDriverWait(driver, 10).until( EC.text_to_be_present_in_element((By.ID, welcome-msg), USERNAME) ) assert USERNAME in driver.find_element(By.ID, welcome-msg).text这段代码的价值不光在于步骤完整更在于断言的时机。点击登录后没有立刻去取页面文案而是先等welcome-msg元素出现并包含指定用户名然后再做断言。如果一上来就find_element大概率会有两种结果要么页面还没跳转找不到报错要么元素找到了但是内容还是空的断言失败。这就是“等待驱动断言”的典型写法。4.3 输入框清空与键盘事件的小细节自动化测试里操作输入框也是常见场景几个小细节我提醒一下。一是输入框里可能已有默认值直接send_keys是把新内容追加在末尾不是覆盖。所以先clear()再输入这是标准动作username_input driver.find_element(By.ID, username) username_input.clear() username_input.send_keys(test_user_01)二是有些日期类输入框会有readonly属性手工可以选日期但send_keys就是无效。这种情况下可以用JavaScript直接给value赋值绕过输入限制driver.execute_script( document.getElementById(date-input).value 2024-06-01; )三是发送键盘按键的场景。比如搜索框里输完关键词想直接按回车触发搜索而不是找搜索按钮from selenium.webdriver.common.keys import Keys search_input driver.find_element(By.ID, search-keyword) search_input.send_keys(自动化测试) search_input.send_keys(Keys.ENTER)这一类细节在真实业务里非常常见文档里不太显眼但是不处理的话用例就是跑不通。4.4 iframe、多窗口与页面滚动如果被测系统嵌入了第三方页面或者支付弹窗你大概率会遇到iframe。iframe就是一个页面里嵌套的另一个独立文档Selenium默认情况下只能操作主文档。进iframe之前必须先切换from selenium.webdriver.common.by import By # 切换到iframe通过id或name driver.switch_to.frame(pay-frame) # 在里面找到按钮并操作 driver.find_element(By.ID, confirm-pay).click() # 操作完成后切回主文档 driver.switch_to.default_content()切换frame的时候有个高频坑在iframe里找不到元素时先检查一下当前Selenium到底在哪个frame。有时候嵌套了两层iframe你得连续切换两次只切一层就会一直报找不到。这块排查一直很费时间我的做法是写一个switch_frame的封装日志里打印当前frame的id避免盲猜。多窗口也是同样的思路。点了新窗口后需要用window_handles切换过去# 记录当前窗口句柄 current_window driver.current_window_handle # 点击触发新窗口打开 driver.find_element(By.ID, open-link).click() # 等待新窗口出现 WebDriverWait(driver, 10).until( lambda d: len(d.window_handles) 1 ) # 切换到最新窗口 new_window [w for w in driver.window_handles if w ! current_window][0] driver.switch_to.window(new_window) # 操作完关闭或切回 driver.close() driver.switch_to.window(current_window)页面滚动这个需求在测试长列表页、加载更多、回到顶部这些场景里很常用。Selenium没有直接封装滚动滚动条的方法需要通过JavaScript来完成# 滚动到页面底部 driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) # 滚动到某个元素位置 driver.execute_script(arguments[0].scrollIntoView(true);, target_element)做“左右滑动”类的横向滚动也是同一套思路把window.scrollTo的第二个参数换成横向滚动的坐标值或者对某个内部滚动容器执行scrollLeft赋值。这类逻辑不复杂但如果没有封装成工具函数每次都要写一遍而且还容易记错API。5. Page Object模式与工程化改造5.1 为什么不能把用例全写在一个文件里自动化测试写多了以后你会发现最大的问题不是“写不出来”而是“维护不起”。如果一个系统的50个用例全部平铺在脚本里页面改一次版你就要去50个地方改定位器新增一个用例你又得复制一长段启动浏览器的代码。这种写法在项目初期跑得挺欢到后面就变成谁也不想碰的定时炸弹。行业里解决这个问题最成熟的办法就是Page Object模式简称PO模式。它的核心思想是把每一个页面封装成一个类类里定义该页面的元素定位和操作方法测试用例只负责场景编排和断言不直接接触定位器。这样分层的收益非常明显页面结构变了只改对应页面类用例逻辑变了只改用例文件页面类可以被多个用例复用。我自己做过的项目里一个注册流程涉及首页、注册页、短信验证、结果页四个页面PO模式让这些页面的操作完全可以独立演进。5.2 一个最小的Page类怎么写我用登录页举例from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class LoginPage: def __init__(self, driver): self.driver driver self.username_input (By.ID, username) self.password_input (By.ID, password) self.submit_btn (By.ID, submit-btn) self.error_msg (By.CLASS_NAME, error-tip) def open(self, url): self.driver.get(url) def login(self, username, password): self.driver.find_element(*self.username_input).send_keys(username) self.driver.find_element(*self.password_input).send_keys(password) self.driver.find_element(*self.submit_btn).click() def get_error_message(self): return self.driver.find_element(*self.error_msg).text注意到没有这里把定位器都放在类属性里而且用的是元组。这样做的好处是当页面改版时你只需要在类顶部修改定位表达式不需要去方法里逐个翻找。login方法封装了“输入账号、输入密码、点击提交”三个动作外部测试用例只需要关心“登录”这个业务动作根本不管底层是ID还是XPath。拿之前的用例改写成PO模式就变得非常干净def test_login_success(driver): login_page LoginPage(driver) login_page.open(BASE_URL /login) login_page.login(USERNAME, PASSWORD) assert USERNAME in driver.find_element(By.ID, welcome-msg).text5.3 公共操作基类的封装每个页面类都会有一些重复动作比如等待元素、截图、滚动、上传文件这些动作属于“所有页面都有”的能力我习惯封装到一个BasePage基类里让其他页面类继承from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: def __init__(self, driver, base_urlNone): self.driver driver self.base_url base_url def wait_visible(self, locator, timeout10): return WebDriverWait(self.driver, timeout).until( EC.visibility_of_element_located(locator) ) def click(self, locator, timeout10): element self.wait_visible(locator, timeout) element.click() return element def fill(self, locator, text, timeout10): element self.wait_visible(locator, timeout) element.clear() element.send_keys(text) return element def get_text(self, locator, timeout10): return self.wait_visible(locator, timeout).text def screenshot(self, name): self.driver.save_screenshot(freports/{name}.png)这样封装带来的变化是测试代码更短、更稳定而且因为所有等待逻辑都收敛在一个地方后面调整全局等待策略时不用到处改。你在团队里推行这套写法时最好先统一“公共方法名”不然每个人自己封装一套click_element、find_and_click项目照样乱。5.4 pytest配合数据驱动pytest框架和Selenium配合起来非常顺手尤其是参数化功能。把一条登录用例扩展成“输入错误密码、输入不存在用户、空密码、正确密码”四种场景只需要一条装饰器import pytest TEST_LOGIN_DATA [ # username, password, expected (user_01, wrong_password, 用户名或密码错误), (no_such_user, test_pass, 用户不存在), (, test_pass, 请输入用户名), (user_01, correct_password, 登录成功), ] pytest.mark.parametrize(username,password,expected, TEST_LOGIN_DATA) def test_login_variants(driver, username, password, expected): login_page LoginPage(driver) login_page.open(BASE_URL /login) login_page.login(username, password) if 登录成功 in expected: assert USERNAME in driver.find_element(By.ID, welcome-msg).text else: assert login_page.get_error_message() expected这就是典型的数据驱动一条用例代码可以被多个数据集复用扩展新场景只是往列表里加一行不用改测试逻辑。同时测试报告里会明确看到每个参数组合是单独一条用例定位失败用例时非常清晰。6. 测试报告、日志与CI集成6.1 pytest插件与报告生成用例写完只是第一步第二步是让结果可读。我当时的痛点是执行完几十条用例控制台输出太简单领导要的是一个能看懂的东西。于是我集成了pytest的报告插件。先装依赖pip install pytest pytest-html执行时加上参数生成HTML报告pytest test_cases/ -v --htmlreports/report.html --self-contained-html--self-contained-html这个参数很关键它把所有样式和脚本都内嵌到HTML文件里单独把这个文件发给别人打开就能看到结果不存在CSS丢失问题。报告里会展示通过数、失败数、每个用例的耗时、失败堆栈基本满足日常汇报需求。如果你希望有更花哨的仪表盘和趋势图可以试试Allure但因为它需要额外的命令行工具公司环境不一定允许装我保守起见一直用pytest-html功能简单直接。6.2 日志与失败截图一个都不能少测试失败之后如果没有日志和截图定位问题等于大海捞针。理想状态是每次失败自动把当时的页面截图保存下来同时把关键操作日志打出来。日志我建议用Python标准库的logging不要用print。原因很简单print只能输出到终端日志却可以分级、写文件、带时间戳。在utils/logger.py里做一次基础配置import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(reports/run.log, encodingutf-8), logging.StreamHandler() ] ) logger logging.getLogger(web_auto)失败截图我通常放在pytest的钩子函数里自动化执行不需要用手动方式去调用。在conftest.py中通过pytest_runtest_makereport这个钩子实现import pytest pytest.hookimpl(hookwrapperTrue) 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_path freports/{item.name}.png driver.save_screenshot(screenshot_path) with open(freports/{item.name}.txt, w, encodingutf-8) as f: f.write(report.longreprtext)这段代码的意思是测试失败时自动把当前页面截图存储下来同时把失败的完整堆栈写进文本。等到排查问题时先看截图确认当时的页面状态再看堆栈定位代码两步结合效率非常高。我有一次排查一个偶现失败就是靠截图发现那个弹层其实已经被关闭了代码却还在点弹层里的按钮——这种问题单看报错信息根本看不出来。6.3 CI环境里的无头Headless模式自动化测试最终要放到CI流水线里执行服务器是没有显示器的。Selenium默认会弹出浏览器窗口这在服务器上是不可能的所以需要启用无头模式。from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headlessnew) # Chrome 109 新版无头参数 options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) driver webdriver.Chrome(optionsoptions)无头模式下页面照样渲染、脚本照样执行只是不显示界面。有几个参数是CI环境里的常客--no-sandbox是因为Linux容器里的用户身份没有沙箱权限不加会直接崩溃--disable-dev-shm-usage是因为容器共享内存太小页面可能加载失败。这两个参数写在启动代码里能避免大多数部署环境下的奇奇怪怪问题。不过无头模式也不是万能和完全等价的有些前端页面的渲染逻辑在无头模式下会有细微差异所以本地调试时我仍然开着窗口只有CI流水线里才会启用无头模式。这个问题我记得很清楚有一次无头模式跑用例全过了手工开浏览器复测却发现页面布局错位。后来查明是某个JS动画在无头模式下直接跳过了导致界面状态不同。所以结论是无头模式适合集成验证不适合完全替代本地手工验证。6.4 定时执行与失败重试机制CI环境跑测试还有个现实问题偶尔网络抖动、某个前端资源加载慢了用例就失败了但这种失败是“假失败”重新跑一遍就过了。解决假失败的通用做法是失败重试。pytest接入pytest-rerunfailures插件后可以在执行命令里指定重试次数和间隔pip install pytest-rerunfailures pytest test_cases/ --reruns 2 --reruns-delay 2意思是每条用例如果失败最多重试2次每次间隔2秒重试之后如果成功则整体算通过。这里有讲究重试次数要控制我最多设2次因为如果重试3次还是失败基本就是真Bug了再多的重试只会掩盖问题。同时一定要看报告的“重试标记”如果一个用例靠重试才跑过最好单独记录一下因为这种用例往往是“不稳定用例”后面要重点排查它为什么不稳定。7. 常见问题与排查技巧实录7.1 元素找不到的标准排查路径“元素找不到”是Selenium自动化里遇到最多的报错报错信息通常是NoSuchElementException或ElementNotFoundError。我在项目里总结了一套固定排查顺序按这个顺序走绝大多数问题五分钟内能定位第一步先看报错时截图如果已经接入失败截图机制。确认页面是不是真的在预期状态。有时候你以为停在登录页其实脚本已经跳到了错误页截图一眼就能看出来。第二步打开浏览器开发者工具手动验证定位表达式。在Console面板里尝试用document.querySelector或原生方法确认目标元素是否在DOM里、是否唯一。如果表达式本身查不到元素那就直接改定位器如果能查到但Selenium依然报错往下走。第三步检查iframe。前面说过Selenium只能操作当前上下文里的DOM。如果目标元素在iframe里必须先switch_to.frame再定位。判断方法就是在开发者工具里搜元素如果Elements面板里能看到iframe嵌套标识基本就是了。第四步检查等待时间。页面是异步渲染的元素可能在3秒后才出现而你的显式等待只设了5秒又恰好这轮渲染慢了自然就找不到。把等待时间调大或者改用更恰当的等待条件问题往往就解决了。第五步检查是否使用了错误的定位方式。有些页面有大量的动态class每次刷新都会带随机后缀如果你用class_name定位这类元素第一次能跑通第二次就崩。稳妥做法是找一个语义化的id或用相对XPath通过稳定属性定位。7.2 元素能定位到但点击报错比找不到更磨人的是“找到了却点不了”典型报错是ElementNotInteractableException或ElementClickInterceptedException。这种问题的本质是元素在DOM里存在但它当前不可交互。常见的场景有三种第一种元素被遮罩盖住了。比如页面上有一个透明的loading蒙层点击目标元素时蒙层还没消失Selenium单击时其实是点在蒙层上。解决办法是等待蒙层消失或者显式等待目标元素可点击后在点击之前额外判断蒙层是否还在。第二种元素处于不可见或不可编辑状态。之前说过的readonly输入框就是这种元素在DOM里但readonlysend_keys无效。这时候用JavaScript赋值是一个路子或者先解除readonly属性再输入。第三种元素在视口之外。有些页面元素虽然渲染出来了但是要滚动到页面下方才能看到和点击。解决办法是点击前先滚动到元素位置用scrollIntoView或者Selenium的ActionChainsfrom selenium.webdriver.common.action_chains import ActionChains target driver.find_element(By.ID, edit-btn) ActionChains(driver).move_to_element(target).click().perform()ActionChains实现的是“移动到目标元素上面再点击”比较接近人的操作通常能绕开一部分滚动可见性问题。7.3 用例偶发失败怎么追“跑十次挂一次”是最让人头秃的问题。我的经验是先把偶发失败当作一个独立Bug去对待而不是靠重试机制蒙混过去。追踪偶发失败的思路主要有三个方向第一看时间点。回看报告里的失败时间和日志如果每次失败都集中在某一分钟而该时段CI服务器刚好在执行构建那大概率是资源竞争导致的页面加载慢。解决方法是给CI流水线增加资源限制或者给关键等待条件加更长超时。第二看元素状态。是否为动态生成、是否涉及随机ID、是否有轮播图/动效。曾经排查过一个问题页面底部的“提交”按钮每隔几秒会被一个广告浮层短暂覆盖恰好脚本点击的瞬间被拦截导致了偶发失败。解决办法就是点击前用显式等待判断浮层消失。第三看数据影响。如果用例依赖生成的测试数据比如订单号、流水号偶发失败常常是数据重复或数据格式变化导致。这种情况需要把测试数据生成逻辑也纳入自动化保证每次使用的是唯一且符合规则的数据。排查偶发问题我强烈建议给每条用例都打上“开始执行时间”和“结束执行时间”的日志这样一份失败报告在手至少能快速判断是否是时序问题。如果连时间戳都没有光靠一条报错堆栈去追偶发效率非常低。7.4 几个小而实用的避坑习惯最后分享几个我在实际项目里总结出来的零碎习惯每一个都是踩过坑才写下来的。第一个send_keys时中文输入偶尔会漏字符尤其在Windows上输入法干扰的时候。稳妥做法是在发送中文字符前先切换到英文输入状态或者使用能绕开输入法干扰的JS赋值方式。这个问题的出现频率不高但一旦出现会让你的用例偶发失败非常难查。第二个不要对driver.title和driver.current_url做即时断言。页面跳转是异步的跳转后地址栏的URL更新和页面内容渲染之间有时间差。我见过很多人断言URL结果慢一点就失败。正确做法是等待某个关键元素出现然后再断言URL或标题。第三个浏览器窗口要最大化。很多前端页面在小窗口下布局会变化比如某些按钮被折叠到汉堡菜单里导致定位不到。在conftest里统一加一行driver.maximize_window()就能减少这整类问题。第四个多套环境配置要用环境变量或配置项区分。我项目里至少有开发、测试、预发三套环境和不同的账号权限用一份配置文件加参数切换比每个环境复制一份脚本要容易维护得多。写到这里关于这套Python Selenium自动化Web测试项目的经验基本都交代完了。我在实际使用中最大的体会是自动化测试的难点从来不是写API调用而是对页面时序的理解和对不稳定因素的治理。把等待机制吃透、把PO结构搭好、把日志和截图作为标配这套框架就不再是一个只能跑通demo的玩具而是真正能替团队守住质量底线的工具。如果你正在从零搭自己的Web自动化项目我的建议是不要急着堆用例先把一条主流程跑稳再逐步扩展你会发现后面的路比想象中顺畅得多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →