我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Selenium+Python Web自动化测试:从回归脚本到框架的完整实践

Selenium+Python Web自动化测试:从回归脚本到框架的完整实践 我做过两年多手工回归测试最崩溃的时候是每天早上花四十分钟重复点同一套流程登录、查询、翻页、核对数据、退出。点完自己都分不清是第几次了。后来我决定用Python和Selenium把这套流程变成脚本彻底从这种重复劳动里解放出来。这篇文章把我从零开始做自动化Web测试的完整过程、踩过的坑、以及从单脚本过渡到框架的真实经验全部写下来适合刚接触自动化测试、被手工回归折磨、或者想系统学习Selenium的人参考。1. 什么情况下你才真的需要Selenium而不是别的东西很多初学者一上来就陷入工具选择焦虑Selenium、Playwright、pytest、Appium看了一圈反而不知道从哪下手。我的看法很简单先搞清楚自己的问题是什么再选工具。1.1 踩到手工测试的临界点手工回归测试的痛苦不是点一次页面而是每天点同一套页面。当你的被测系统稳定下来、功能模块变多以后每条主流程回归一遍需要的时间是线性增长的。我第一次意识到必须自动化是因为一个版本上线前我花了整整一个下午回归了十二个模块结果第二天产品经理说要加一个字段整个流程又得重来一遍。那一刻我明白了一个道理手工测试适合探索性测试适合判断这个功能好不好用但绝不适合重复验证这个功能还正不正常。回归测试的本质是重复重复的事情就该交给脚本。另外一个信号是你开始用Excel记录测试步骤或者你带的新人问你这个流程怎么点而你不想再次演示。这说明你的测试知识还锁在你的脑子里没有沉淀成可执行的东西。Selenium脚本本身就是测流程的可执行文档别人拿过去能跑这才是价值。1.2 Selenium、pytest、Playwright这些名字到底什么关系这是新手最容易混乱的地方。我建议你把它们分成两类。第一类是浏览器自动化库包括Selenium、Playwright等。它们负责驱动真实浏览器模拟人去点击、输入、翻页。Selenium是其中资历最老的从2004年就存在生态成熟、资料多、支持Java/Python/JavaScript等几乎所有主流语言。第二类是测试运行框架比如pytest。它负责组织用例、断言结果、生成报告、控制执行顺序。Selenium管怎么操作浏览器pytest管哪些操作算一个用例、失败后怎么处理、怎么批量跑。所以Selenium和pytest不是竞争对手而是配合关系。你完全可以只写Selenium脚本不引入pytest但那就像写菜谱却没有目录一样最后会乱成一锅粥。至于和Playwright比较我会在后面给出我的判断。1.3 Selenium能做什么、不能做什么很多教程把Selenium吹成万能工具实际它会让你在一些场景下非常难受。我把Selenium的能力边界画清楚能做Web界面的功能测试、跨浏览器兼容性测试、重复流程的回归验证、需要模拟用户真实操作的自动化任务。能做但不擅长验证码识别图形验证码基本无解别浪费时间、大量并发操作它是真实浏览器驱动不是压测工具。不能做接口性能测试那是JMeter、Locust的事、纯后端逻辑测试。我见过有人试图用Selenium做百万级数据爬取结果浏览器内存占用高得吓人速度还慢最后不得不改用接口脚本。Selenium的设计初衷是模拟用户不是快速执行。认清楚这个边界你后面才不会选错工具。2. Selenium的核心运行逻辑WebDriver、浏览器与脚本的三角关系很多教程直接让你安装库然后写代码但从来不解释为什么跑起来需要多一个驱动程序。我刚开始也是照着抄结果碰到驱动版本不匹配的报错直接懵了。这一章把底层逻辑讲透。2.1 WebDriver协议到底在做什么我打一个比方Selenium脚本是中文用户浏览器是只说英文的英国人WebDriver就是一个随身翻译。你的Python脚本通过WebDriver协议发指令翻译成浏览器能听懂的原生操作指令浏览器的状态通过反向翻译成你的代码能读懂的返回值。在Selenium 3时代这个翻译过程是有一个独立的可执行文件在本地监听端口完成的通常叫chromedriver.exe或geckodriver.exe。到了Selenium 4负责翻译的组件被内嵌进库里面叫Selenium Manager它会自动去找驱动、下载驱动省去了手动配置的痛苦。但我要提醒你Selenium Manager自动下载驱动依赖网络下载渠道如果你有内网环境或者下载不畅手动下载驱动仍然是必备技能。2.2 驱动版本必须匹配这个坑90%的人踩过浏览器的驱动不是通用的。Chrome浏览器的驱动必须和你本机Chrome的版本对应差一个大版本都会直接报SessionNotCreatedException。我举个例子本机Chrome是126版你下载的chromedriver是125版运行时就会看到这样的报错SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 125解决方案很直接打开Chrome的设置-关于Chrome看版本号然后去ChromeDriver下载页面选对应版本。Chrome大版本更新很快所以驱动更新也是常态不要指望一劳永逸。后来我干脆在代码里加了一个小函数每次启动前自动读取浏览器版本然后打印出来省得排查半天才发现是驱动过期。2.3 三条指令链路的完整理解一次最简单的打开页面的操作背后是这样一条链路脚本创建WebDriver实例启动浏览器进程执行driver.get()方法脚本把打开这个URL翻译成WebDriver协议指令WebDriver把指令发送给浏览器驱动进程驱动进程调用浏览器原生接口浏览器开始加载页面浏览器把加载状态返回给驱动驱动再返回给脚本理解这条链路对排查问题很有帮助。比如脚本卡住了你要判断是哪一段出现问题是浏览器压根没起来说明驱动或启动配置有问题还是浏览器起来了但页面加载失败说明URL或网络有问题还是页面加载了但元素找不到说明定位或等待有问题。这三个阶段对应完全不同的排查方向。3. 环境搭建虚拟环境、浏览器驱动与第一个能跑的脚本很多人卡在环境搭建这步。其实只要摸清楚四个环节就不会乱了Python环境、Selenium库、浏览器驱动、代码本身。每个环节都有一些文档不会细讲的点。3.1 用虚拟环境隔离你的Selenium项目我不建议直接在全局Python环境里pip install selenium。因为你可能同时在做多个项目A项目需要Selenium 3B项目需要Selenium 4全局环境很快就冲突了。推荐用venv创建虚拟环境。操作非常简单# 创建项目目录并进入 mkdir selenium_demo cd selenium_demo # 创建虚拟环境 python -m venv venv # 激活虚拟环境Windows venv\Scripts\activate # 激活虚拟环境macOS/Linux source venv/bin/activate # 安装Selenium pip install selenium装完之后验证一下版本pip show selenium看到版本号输出就说明安装成功了。我建议顺手装一个pytest后面写框架会用得上pip install pytest有个小技巧可以提前做把你的依赖记录到requirements.txt里后面换电脑或者同事接手你的项目一条命令就能恢复环境。pip freeze requirements.txt3.2 浏览器驱动下载最容易搞错的两个细节如果你用Selenium 4.6以上版本理论上Selenium Manager会自动下载驱动。但实际使用中自动下载偶尔会因为网络问题失败。我的建议是掌握手动下载驱动的完整流程这是基本功。以Chrome为例操作分三步查看Chrome版本号。地址栏输入chrome://version或者点设置-关于Chrome。打开ChromeDriver下载页面找到和Chrome版本号对应的驱动版本。注意主版本号一致即可比如Chrome 126.x.x用ChromeDriver 126.x.x。下载后解压得到一个可执行文件。把这个文件放到一个固定目录并把目录加入系统PATH或者在代码中直接指定路径。这里有两个细节经常把人搞晕。第一个是下载页面有多个版本节点不要只看最新版必须看对应版本第二个是Mac电脑首次运行下载的驱动会被系统拦截需要去系统设置-隐私与安全性里允许运行。知道这两点能省很多摸索的时间。3.3 第一个能跑的脚本打开百度并断言标题环境装好之后写第一个脚本不要搞复杂逻辑只做一件事打开浏览器、访问页面、检查标题、退出。我把验证环境是否通行的脚本放这里你复制粘贴就能跑from selenium import webdriver from selenium.webdriver.common.by import By import time driver webdriver.Chrome() # 如果你手动下载了驱动可以传入路径如 executable_path/path/to/chromedriver try: driver.get(https://www.baidu.com) time.sleep(2) title driver.title print(页面标题:, title) assert 百度 in title print(测试通过) finally: driver.quit()这段代码有两点值得解释。第一webdriver.Chrome()用了Selenium 4的写法Selenium Manager会自动找驱动。第二time.sleep(2)是个临时手段实际项目中不要用它后面我会详细讲为什么等待机制有更优雅的解法。3.4 我建议的目录结构很多人脚本越写越多之后就开始乱所有文件堆在一个文件夹里。我从一开始就养成了分目录的习惯推荐你参考这个结构selenium_demo/ ├── venv/ # 虚拟环境 ├── drivers/ # 浏览器驱动可执行文件 ├── pages/ # 页面对象Page Object模式 ├── tests/ # 测试用例 ├── reports/ # 测试报告 ├── config.py # 全局配置 └── requirements.txt # 依赖清单前期可以不严格遵循但至少做到测试用例和公共代码分离。这样做有一个实际好处当你的用例从10个增加到100个时你不会在找文件上浪费生命。4. 高频操作背后的API逻辑定位、交互与断言的真功夫Selenium的全部能力归结起来就是三个动作找到元素、操作元素、读取结果。这个章节我把最常用的API串起来讲每个都配有选择依据而不是简单罗列。4.1 八种定位方式实际用下来优先级是这样Selenium提供了八种元素定位方式ID、Name、Class Name、Tag Name、CSS Selector、XPath、Link Text、Partial Link Text。新手常常纠结用哪个我直接给你我实践后的选择顺序优先级定位方式适用场景稳定性1ID几乎每个元素都可能有的唯一标识最稳2CSS Selector无ID时的首选语法简洁很稳3XPath页面结构复杂、CSS难以表达时较稳4Name表单类元素常见属性有时稳5Class Nameclass属性唯一时可用不稳6Tag Name定位同类元素集合时用不稳7Link Text精确匹配超链接文字一般8Partial Link Text模糊匹配超链接文字一般ID是所有定位方式里性能最好的也是开发者最可能保持稳定的属性。但实际工作中页面经常没有ID或者ID是动态生成的比如每次刷新都会变这时候我优先看CSS Selector。XPath虽然功能强大但是容易写出冗长易碎的表达式能不用就不用。举个实际例子我要定位登录页的用户名输入框在浏览器开发者工具里看到结构是这样的input typetext nameusername idusername_input classform-control那么三种写法分别是这样# ID定位优先 driver.find_element(By.ID, username_input) # CSS定位备选 driver.find_element(By.CSS_SELECTOR, input[nameusername]) # XPath定位不得已时 driver.find_element(By.XPATH, //input[idusername_input])4.2 把点击、输入、下拉、勾选这些动作翻译成代码元素定位到之后操作本身其实不复杂但有几个动作的细节值得讲。点击和输入# 找到并输入 username_input driver.find_element(By.ID, username_input) username_input.clear() # 先清空这是很多人忽略的步骤 username_input.send_keys(test_user) # 找到并点击按钮 login_btn driver.find_element(By.ID, login_btn) login_btn.click()为什么要先clear()因为输入框里可能有默认值或者上一次输入的内容没有清空直接send_keys会把内容追加在末尾导致断言失败。这个细节我调试了半小时才意识到。下拉选择框需要用Select类处理from selenium.webdriver.support.ui import Select # 定位下拉框 select_element driver.find_element(By.ID, city_select) # 包装成Select对象 select Select(select_element) # 三种选择方式 select.select_by_visible_text(北京) # 按显示文字 select.select_by_value(beijing) # 按value属性 select.select_by_index(2) # 按下标从0开始勾选框的点击逻辑值得注意如果用element.is_selected()判断当前状态再决定是否点击就不会出现越点越反的困惑。checkbox driver.find_element(By.NAME, agree) if not checkbox.is_selected(): checkbox.click()4.3 断言这个动作才是测试的核心价值自动化测试和自动操作的关键区别在于不仅要做动作还要验证结果。Selenium本身没有断言功能但你可以用Python自带的assert语法。常用断言对象有这么几类# 断言页面标题 assert 后台管理系统 in driver.title # 断言当前URL assert login in driver.current_url # 断言元素文本 text driver.find_element(By.CLASS_NAME, welcome_msg).text assert 欢迎你 in text # 断言元素是否可见 from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC is_displayed driver.find_element(By.ID, success_icon).is_displayed() assert is_displayed断言写得好不好直接决定报错时你能不能一眼定位问题。我见过有人把所有断言都写在最后一行结果一个元素失败导致后面全部不执行。建议每完成一步关键操作就立刻断言这一步的结果比如登录成功后立即断言页面标题变了不要等到最后统一检查。5. 实战案例一个带登录、条件查询和结果校验的完整测试流程现在把前面所有知识点整合到一个真实场景里。假设被测系统是一个后台管理系统流程是登录系统、进入用户列表页、按条件查询用户、校验查询结果。5.1 被测页面的功能拆解和脚本设计思路我先手动画一个被测页面的逻辑图方便你理解流程登录页用户名输入框、密码输入框、登录按钮。登录成功后跳转到首页首页左侧有导航菜单。用户列表页顶部有搜索条件区包括用户名输入框、状态下拉框、查询按钮、重置按钮。下方是结果表格包含用户名、状态、创建时间等列。我的脚本设计思路是一个函数负责登录另一个函数负责查询第三个函数负责验证结果。三个函数清晰分离任何一个环节出问题报错信息能直接定位到是登录失败、还是查询按钮没生效、还是数据没刷新。5.2 完整脚本逐段解读下面是去掉无关细节的完整脚本我加了详细注释import time 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 selenium.webdriver.support.ui import Select # 全局配置 BASE_URL https://example-admin.com USERNAME admin PASSWORD admin123 def login(driver, username, password): 登录系统 driver.get(BASE_URL /login) # 显式等待登录表单加载完成 WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, username)) ) username_input driver.find_element(By.ID, username) password_input driver.find_element(By.ID, password) username_input.clear() username_input.send_keys(username) password_input.clear() password_input.send_keys(password) driver.find_element(By.ID, login_btn).click() # 等待登录成功后的首页元素出现 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, dashboard)) ) print(登录成功) def search_user(driver, keyword, status): 按条件查询用户 # 点击左侧导航进入用户列表页 user_menu driver.find_element(By.XPATH, //span[text()用户管理]) user_menu.click() time.sleep(1) # 页面跳转过渡 # 输入查询条件 keyword_input driver.find_element(By.ID, search_keyword) keyword_input.clear() keyword_input.send_keys(keyword) status_select Select(driver.find_element(By.ID, search_status)) status_select.select_by_visible_text(status) # 点击查询按钮 driver.find_element(By.ID, search_btn).click() # 等待结果表格刷新 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, table tbody tr)) ) print(查询完成) def verify_result(driver, expected_keyword): 校验查询结果 rows driver.find_elements(By.CSS_SELECTOR, table tbody tr) assert len(rows) 0, 查询结果为空 first_row_text rows[0].text assert expected_keyword in first_row_text, f结果中未找到关键字{expected_keyword} print(结果校验通过共返回, len(rows), 条数据) # 主流程 driver webdriver.Chrome() try: login(driver, USERNAME, PASSWORD) search_user(driver, 张三, 启用) verify_result(driver, 张三) finally: driver.quit()5.3 为什么把登录单独抽成一个函数实战中登录功能会被大量用例复用如果每个用例都重新写一遍登录代码维护成本会成倍上涨。一旦登录按钮的ID变了你要改的可能是几十个文件。把登录抽成函数后只需要改一处。这里的经验是你对变化要有预判。页面中哪些内容最可能变化元素的ID、按钮文字、布局路径这些都要尽量集中管理。更进一步把元素定位信息和操作逻辑分离才是框架级做法这个我留在后面Page Object章节详细展开。5.4 跑通流程之后我做的第一件事是看测试报告很多人跑通脚本后就很兴奋地收工了但这时候其实只完成了一半。真正的检验是你故意引入一个Bug比如把查询按钮改成无效再跑一次脚本看它能不能准确报出来。如果脚本在错误发生时报错信息不够明确你就必须优化代码因为真实测试中这个Bug可能就是那个查询结果不正确的问题。我常用的做法是给每个函数增增加日志输出像上面的print就是最简单的方式。后面引入pytest之后可以直接用pytest的日志系统哪些步骤失败、失败发生在哪一行一目了然。6. 脚本又飘了的真相等待、iframe、新窗口和弹窗你辛辛苦苦写完脚本跑第一次可能就失败了或者今天能跑明天不能跑。我把这些让脚本飘的常见原因逐个拆解每一类都有实际的操作解法。6.1 显式等待和隐式等待为什么time.sleep不是好方案所有Web自动化新人都会经历一段sleep修仙期——页面加载不出来就在代码里到处加time.sleep。我也这么干过。但sleep最大的问题是时间写多了浪费执行时间、写少了页面没加载完照样报错而且本机网速变化一快一慢就让脚本变得极其不稳定。Selenium真正推荐的做法有两种隐式等待在一次session级别设置轮询查找元素在设定时间内找不到才报错。driver.implicitly_wait(10) # 对后续所有find_element生效但隐式等待有个坑它只对元素查找有效对元素存在但不可见元素被遮挡不可点击这类场景无能为力。而且隐式等待和显式等待混用时会产生叠加效果反而让等待时间变长。显式等待针对特定条件比如元素可见、元素可点击、元素出现某段文本等设定最长等待时间轮询检查直到条件满足。element WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, submit_btn)) )我强烈推荐显式等待它表达的是等到这个条件满足再继续而不是等固定几秒。多个显式等待之间有天然的顺序性脚本可读性也更强。常见条件我列几个presence_of_element_located元素出现在DOM中visibility_of_element_located元素可见element_to_be_clickable元素可点击这个最实用text_to_be_present_in_element元素中出现指定文本element_to_be_selected元素已选中6.2 iframe切换找不到元素的头号隐形杀手你按ID搜索一个元素明明在浏览器开发者工具里能看到它但Selenium就是报NoSuchElementException。这种情况下八成是遇到了iframe。iframe是页面里嵌套的另一个文档Selenium默认只能访问最外层文档。如果目标元素在iframe里就必须先切换到这个iframe操作完成后再切回来。# 切换进入iframe driver.switch_to.frame(main_iframe) # 通过name或id # 定位iframe内部的元素 driver.find_element(By.ID, inner_input).send_keys(hello) # 操作完成后切回主文档 driver.switch_to.default_content()判断是否在iframe里有一个快捷方法在浏览器开发者工具的Elements面板里搜索这个元素如果它的上级节点里有iframe标签那就需要切换。我在实际项目中发现很多系统集成了第三方编辑器、地图组件、支付组件这些组件就经常用iframe嵌套所以在这些区域里操作前一定要记得切换。6.3 点击后新开标签页窗口句柄的切换逻辑很多系统点击链接后会在新标签页打开尤其是打开外部链接或查看详情页。如果不做窗口切换你依然停留在旧页面自然找不到新页面里的元素。Selenium切换窗口的逻辑并不复杂# 记录当前窗口句柄 main_window driver.current_window_handle # 点击触发新标签页 driver.find_element(By.ID, detail_link).click() # 等待新窗口出现 WebDriverWait(driver, 10).until( EC.number_of_windows_to_be(2) ) # 切换到最新打开的窗口 new_window [w for w in driver.window_handles if w ! main_window][0] driver.switch_to.window(new_window) # 操作完新窗口后切回原来的窗口 driver.close() driver.switch_to.window(main_window)这里有两个细节。第一不要用driver.switch_to.window(driver.window_handles[-1])虽然大多数情况下它是对的但如果有多个窗口时容易切换错。第二关闭新窗口后要切回主窗口否则后面操作会一直找不到元素。6.4 alert弹窗的处理时机Web页面有三种原生弹窗alert、confirm、prompt。它们会阻塞页面操作必须显式处理。处理方式完全一样只需要判断是要接受还是取消# 获取弹窗对象 alert driver.switch_to.alert # 读取弹窗文案可以用于断言 text alert.text assert 操作成功 in text # 接受弹窗 alert.accept() # 如果需要对confirm弹窗点击取消用 # alert.dismiss()注意时机弹窗出现后要尽快处理否则脚本会一直卡住。我见过有人在弹窗出现后又去执行别的操作导致超时的案例这类弹窗处理顺序是先切到alert、读取文本、执行接受或取消、继续后续操作。7. 一次真实排查实录定位器没有任何问题却一直报TimeoutException这一章我分享一次真实的排查过程它的特殊之处在于所有元素定位器都用开发者工具复制下来确认无误代码逻辑也没有问题但脚本就是跑不通。这种问题最折磨人。7.1 问题现象那是一个数据报表页面点击导出按钮后页面会弹出一个下载提示条上面有一个下载链接。我的脚本设计是点击导出按钮等待下载提示条出现然后点击其中的下载链接。脚本运行后一直报TimeoutException: Message: timeout: Timed out receiving message from renderer: 299.836定位器是从开发者工具里复制出来的元素确实存在。我用隐式等待也试过手动操作页面又一切正常。这个问题整整折腾了一个下午。7.2 我的完整排查链路我按这个顺序进行排查第一步打印当前页面URL和标题确认页面没有跳转到别的地方。第二步打印页面HTML源码搜索下载这个关键词发现源码里根本没有我要的下拉那个提示条相关的HTML元素。第三步在浏览器手动操作搜索这个下载提示条所在的HTML代码位置发现它不属于页面文档本身而是一个被浏览器弹出的下载提示区域。这就是根因点击导出后触发了浏览器原生下载行为而不是页面跳转渲染。原生下载提示属于浏览器UI属于浏览器外部界面Selenium模拟的是网页用户操作行为它根本触碰不到所以永远找不到这个元素。7.3 根因与修复方案弄清根因以后修复思路就清晰了。既然浏览器原生下载提示抓不到那就不要走这一套配置浏览器自动下载到指定目录不弹提示然后通过检查下载目录中是否出现新文件来判断下载是否成功。具体实现是给Chrome添加下载选项from selenium import webdriver options webdriver.ChromeOptions() prefs { download.default_directory: /path/to/downloads, # 下载目录 download.prompt_for_download: False, # 不提示 download.directory_upgrade: True, safebrowsing.enabled: True } options.add_experimental_option(prefs, prefs) driver webdriver.Chrome(optionsoptions)脚本逻辑变成点击导出检查指定下载目录中文件数量增加或文件大小变大。这个方案稳定得多因为下载行为本身变成了文件系统层面的验证与浏览器UI完全解耦。7.4 这类神秘超时的通用排查顺序这次排查看似偶然其实有通用的方法可循。后来我把这类问题总结成一套排查路顺序遇到脚本异常直接按这个流程走打印当前页面URL与标题确认是否已经跳转。打印页面源码确认目标元素是否真的存在于当前文档中。在浏览器开发者工具的Console中手动执行document.querySelector确认元素在当前DOM中。检查元素是否在iframe里或是否在Shadow DOM内部。检查是否触发了浏览器原生UI下载框、弹窗、通知这些Selenium无法捕获。检查网络请求是否被拦或被延迟必要时在脚本里配置网络条件。这套顺序能覆盖我遇到的九成问题。特别提醒一句第5步最容易忽略也最耗时。一旦你发现元素在源码中不存在优先怀疑是不是浏览器原生UI而不是反复检查定位器。8. 从单脚本到自动化框架pytest集成、Page Object与无头模式单脚本能跑通只是起点。要想让自动化测试真正能持续维护、能跑大量用例、能集成到日常开发流程里必须进阶到框架层面。这一章讲我实际用到的三个改造。8.1 为什么能跑的脚本还不是自动化测试框架单脚本有一个致命问题数据、定位器、操作逻辑、断言混在一起用例一多就失控。比如你有20条用例每条都是独立脚本每个脚本里都有一段登录代码。有一天登录流程加了图形验证码你就要改20个地方。引入测试框架的本质是把用例从脚本中抽离出来。用例只描述测什么、预期什么而怎么做交给公共函数和页面对象。pytest恰好提供了这个抽象体系它的fixture机制、断言收集机制天然适合承载Selenium测试。8.2 pytest fixture管理浏览器生命周期fixture是pytest最核心的功能。我用来管理浏览器的创建和退出这样每条用例执行前后都会自动分配一个干净的浏览器实例且用完自动关闭不会互相干扰。import pytest from selenium import webdriver pytest.fixture(scopefunction) def driver(): 每个用例分配一个干净浏览器 options webdriver.ChromeOptions() options.add_argument(--window-size1920,1080) chrome_driver webdriver.Chrome(optionsoptions) yield chrome_driver chrome_driver.quit()scopefunction表示每个测试函数都重新创建一个浏览器。如果你的用例之间对页面状态没有依赖这是最稳的模式。也可以改成scopemodule让一个模块共用一个浏览器速度更快但用例间可能互相影响我建议前期都用function级别。在测试用例里直接接收这个fixture参数即可def test_login_success(driver): driver.get(BASE_URL /login) # 后续操作... assert 首页 in driver.title8.3 Page Object模式一次改造受用终身Page Object模式是Selenium官方推荐的实践核心思想是每个页面对应一个类页面上的元素和操作封装成这个类的方法。测试用例调用方法不直接接触定位器。拿登录页面举例# pages/login_page.py 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 def navigate(self, url): self.driver.get(url) def enter_username(self, username): el WebDriverWait(self.driver, 10).until( EC.visibility_of_element_located((By.ID, username)) ) el.clear() el.send_keys(username) def enter_password(self, password): el self.driver.find_element(By.ID, password) el.clear() el.send_keys(password) def click_login(self): self.driver.find_element(By.ID, login_btn).click() def login(self, username, password): self.enter_username(username) self.enter_password(password) self.click_login()测试用例变成这样# tests/test_login.py from pages.login_page import LoginPage def test_login_success(driver): login_page LoginPage(driver) login_page.navigate(BASE_URL /login) login_page.login(admin, admin123) assert 首页 in driver.title改造完之后如果页面定位器变了你只需要维护Page类里的定位信息如果测试步骤变了你只需要改测试用例里的调用顺序。两层解耦让维护成本大幅下降。这个模式在实战项目中经受住了几千条用例的考验。8.4 无头模式与CI环境跑法自动化测试真正的战场是CI环境比如每次代码提交后自动跑一遍回归。但CI机器上没有显示器浏览器必须用无头模式运行。Chrome的headless模式不需要图形界面也能完整执行JavaScript、渲染页面。options webdriver.ChromeOptions() options.add_argument(--headlessnew) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--window-size1920,1080) driver webdriver.Chrome(optionsoptions)如果想要在本地调试时看到浏览器界面、在CI时自动切换无头模式可以加一个环境变量控制import os options webdriver.ChromeOptions() if os.getenv(HEADLESS): options.add_argument(--headlessnew) driver webdriver.Chrome(optionsoptions)--headlessnew是Chrome新无头模式比老的更可靠。--no-sandbox和--disable-dev-shm-usage是容器环境常见的问题修复不加的话在Docker里经常会莫名其妙崩溃。这几个参数的组合我建议直接照抄它们是CI环境跑Selenium的标准配置。无头模式下脚本执行速度通常更快但偶尔会出现截图空白或某些元素交互异常的情况所以我的建议是本地开发调试用有头模式CI环境用无头模式发现问题时先在本地复现再检查。这样两边各取所长。最后再分享一点实际操作中的体会做Web自动化测试最大的价值不在于省了多少手工点击的时间而在于它强迫你理解页面背后的结构逻辑、数据流和边界情况。你会开始思考按钮为什么要点两次、弹窗为什么会延迟、接口和UI之间到底谁先返回。这种额外的理解会让你的测试质量明显变高而这也是自动化测试真正值得投入的原因所在。
返回列表