ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Selenium浏览器自动化实战:从环境搭建到pytest集成

Selenium浏览器自动化实战:从环境搭建到pytest集成 1. 先说清楚Selenium到底是什么1.1 它解决的问题做Web测试的朋友不管你是刚开始接触自动化还是已经写过几年脚本大概率都绕不开Selenium这个名字。它是目前生态最成熟、资料最全的浏览器自动化工具没有之一。我见过不少新手把Selenium当成一个测试工具其实这个理解不太准确。Selenium说白了就是一台浏览器遥控器——它通过WebDriver协议跟浏览器通信模拟真实用户的操作打开页面、点击按钮、填写表单、滚动屏幕、切换标签页这些你手动能做出来的动作它都能代替你做而且做得比你快、比你稳定、比你重复一万次都不带抱怨。它能解决什么问题往大了说回归测试。项目迭代快的时候每次发版前最怕的是什么不是开发改出bug而是改了一个看似无关的模块结果把老功能搞挂了。靠人肉回归一遍主流程少说两三个小时要是天天发版本测试同学直接崩溃。自动化脚本跑一遍十几分钟出结果有问题的点直接截图给你看效率完全不在一个量级。往小了说它还能帮你干一些脏活累活。比如批量填表、批量数据录入、定时抓取页面信息这些工作本身就是机械重复的用脚本做再合适不过。我在实际项目里甚至拿Selenium帮运营部门做过批量导出报表的辅助工具。1.2 适合谁来学如果你属于下面这几类人Selenium值得投入时间去学测试工程师日常要写用例、做回归掌握自动化是提升产出和话语权的硬技能开发工程师偶尔需要自测前端页面流程或者想给项目搭个冒烟测试用Selenium比手动点浏览器省事得多运维/数据分析师经常要跟网页交互、取数很多重复性浏览器操作完全可以用脚本替代。不需要你有多深的编程基础懂一点Python基础语法就能上手。我在文章里会尽量把每一步的原理和坑都讲清楚照着做你大概一个下午就能把环境跑起来写出第一个能用的自动化脚本。2. 环境搭建和核心选型2.1 为什么选PythonSelenium官方支持的语言很多Java、Python、C#、Ruby、JavaScript都有。但这些年下来Python基本成了国内做Web自动化测试的事实标准原因很直白语法简洁写起来快调试成本低而且相关的生态pytest、Allure、数据驱动、CI集成配合得非常好。一个自动化脚本用Java写可能要五十行Python二十行就搞定。不是Java不好而是测试脚本这种要快速迭代、不要复杂工程的场景Python的轻量特性天然匹配。尤其当你需要临时改一个元素定位、加一条断言Python这边改完直接跑Java还得重新编译打包走一套流程那个酸爽我到现在都记得。2.2 从安装到第一个脚本环境准备很简单总共三步第一步装Python。建议直接装Python 3.8以上的版本。装的时候注意勾选“Add Python to PATH”不然后面命令行里敲python会提示找不到命令。第二步安装Selenium库。终端里执行pip install selenium这里提醒一句不要用pip install selenium装完就觉得万事大吉装完以后确认一下版本。pip show selenium第三步安装浏览器驱动。这是整个环境搭建里最容易踩坑的一步后面专门展开讲。装完依赖先写一个最简单的冒烟脚本验证环境通不通from selenium import webdriver driver webdriver.Chrome() driver.get(https://www.baidu.com) print(driver.title) driver.quit()如果这脚本能跑起来并且在终端里打印出页面标题说明Selenium环境已经通了。后面所有复杂操作都是在这个基础上往上叠加的。2.3 浏览器驱动的那些坑很多新手环境装完跑脚本的时候会报WebDriverException: Message: chromedriver executable needs to be in PATH。这个报错十有八九是驱动没配好。Chrome浏览器和ChromeDriver之间有一个严格的对映关系Chrome版本升级驱动必须跟着升级否则就报版本不匹配。你本地Chrome是120手里的ChromeDriver还停在114运行的时候直接抛异常。解决办法是到ChromeDriver的下载页面找到跟你浏览器主版本号一致的驱动文件下载后解压把可执行文件放到Python的Scripts目录下或者放到任意目录然后把目录路径加进系统环境变量PATH。我个人的习惯是用Selenium Manager。从Selenium 4.6版本开始官方内置了这个工具它会在你运行脚本的时候自动检测浏览器版本、自动下载匹配的驱动。不需要手动维护驱动的版本这真是省了不少事。用法没有任何变化from selenium import webdriver driver webdriver.Chrome() # Selenium Manager自动处理驱动但要注意一点Selenium Manager在某些内网环境或者代理受限的环境下会下载失败。如果遇到这类问题还是得回到手动下载驱动的路子。另外如果公司内部网络访问不了外网还有一个方案把驱动文件放到项目目录下用Service类指定路径from selenium import webdriver from selenium.webdriver.chrome.service import Service service Service(rC:\tools\chromedriver.exe) driver webdriver.Chrome(serviceservice)2.4 无头模式配置脚本写多了你大概会想让它在后台跑不弹出浏览器窗口。这就是headless模式。好处是不占桌面、跑起来更快、适合挂在服务器上定时执行。from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headless) options.add_argument(--window-size1920,1080) driver webdriver.Chrome(optionsoptions)这里有一个非常实用的细节--window-size一定要设置。因为在无头模式下浏览器窗口默认是800x600有些页面在窄屏下会触发响应式布局元素位置跟正常窗口完全不一样。你原本用CSS定位能点到的按钮可能就被折叠进汉堡菜单里了定位直接失效。被这个问题坑过几次之后我现在凡是写无头模式第一行必定加上窗口尺寸。3. 八个高频操作的实战细节3.1 元素定位别只会用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能力最强但写不好会很长很脆动不动就失效。举个例子这个元素button idsubmit-btn classbtn primary>el driver.find_element(By.ID, submit-btn)如果没有id可以试试CSSel driver.find_element(By.CSS_SELECTOR, button.btn.primary)CSS还在万不得已的时候用XPathel driver.find_element(By.XPATH, //button[contains(class, primary) and text()登录])但我要提醒一个很多人忽略的点不要图省事复制浏览器的绝对路径XPath。那种/html/body/div[3]/div[2]/div/ul/li[5]/a看起来能用实际上前端工程师改一个层级就整个报废。尽量用相对关系、文本属性、class特征来定位稳定性会好很多。另外关于元素定位的推荐级别我想补充一个更实用的思路现在很多页面用了自动化测试专用的>driver.implicitly_wait(10)这种方式比sleep聪明一些但它是全局设置只对元素查找生效对页面重载、Ajax数据回填这些场景无能为力。显式等待WebDriverWait这是最推荐的方式针对某个特定的条件等待精确、可控、稳定。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait WebDriverWait(driver, 10) element wait.until(EC.element_to_be_clickable((By.ID, submit-btn))) element.click()显式等待的好处是等待条件很具体比如“按钮可点击”、“元素可见”、“元素存在”一旦条件满足立即继续执行不需要傻等。如果超时了它会抛出TimeoutException并且你能明确知道是哪个元素没出现排查问题非常方便。我在真实项目中几乎所有的关键交互都会配合显式等待。宁可等待条件写得细一点也不要图省事把等待全部交给隐式设置。3.3 常用交互操作点击、输入、勾选定位到元素之后最常用的是输入和点击# 输入文本 username_input driver.find_element(By.ID, username) username_input.clear() username_input.send_keys(tester01) # 点击按钮 login_btn driver.find_element(By.ID, login-btn) login_btn.click()很多人写到这里就完了其实有细节要注意。clear()最好每次输入前都调用一下。有些页面的输入框有默认值或者上一次脚本残留了旧文本不清理直接send_keys往往是拼接的结果而不是替换。这个小坑我亲眼见过不少次。对checkbox和radio直接click()即可agree_checkbox driver.find_element(By.CSS_SELECTOR, input[typecheckbox][nameagree]) if not agree_checkbox.is_selected(): agree_checkbox.click()对下拉框select元素建议直接用Select类比自己模拟点击要省心得多from selenium.webdriver.support.ui import Select select_el Select(driver.find_element(By.ID, city)) select_el.select_by_value(330100) # 按value选 select_el.select_by_visible_text(杭州) # 按可见文本选 select_el.select_by_index(2) # 按索引选3.4 文件上传与下载文件上传是自动化测试里一个绕不开但是很烦的场景。因为浏览器自带的上传弹窗是系统级窗口Selenium本身没办法直接操作那个弹窗。最常见的解决方式是用send_keys()直接给input typefile元素发送本地文件路径file_input driver.find_element(By.CSS_SELECTOR, input[typefile]) file_input.send_keys(rC:\Users\test\files\data.xlsx)这里的关键在于不管上传控件在页面上显示成什么样子隐藏的input、自定义的拖拽区域底层几乎都是有一个input typefile元素的。找到它直接send_keys路径就能触发上传。这个方法稳定、跨平台是我处理上传功能的首选。如果遇到的是那种把input藏得很深的用js强行去掉隐藏属性再操作也行file_input driver.find_element(By.CSS_SELECTOR, input[typefile]) driver.execute_script(arguments[0].style.display block;, file_input) file_input.send_keys(C:/test/upload.csv)下载方向的自动化比上传要难一些因为涉及浏览器下载行为。我的建议是设置下载目录并配置自动下载options Options() prefs { download.default_directory: rD:\downloads, download.prompt_for_download: False, download.directory_upgrade: True, } options.add_experimental_option(prefs, prefs)配置之后点击下载按钮文件会直接保存到指定目录。为了确保文件真的下载完了还需要配合轮询import os import time def wait_for_download(dir_path, timeout30): while timeout 0: files [f for f in os.listdir(dir_path) if not f.endswith(.crdownload)] if files: return files[0] time.sleep(1) timeout - 1 raise TimeoutError(下载超时)这里判断.crdownload后缀很重要因为Chrome下载过程中文件还没写完会带这个临时后缀。直接判断目录里有没有文件很可能刚创建临时文件就被你当成下载完成了后续解析文件就会失败。3.5 弹窗处理页面上的弹窗分两类处理方式完全不同。第一类浏览器原生alert/confirm/prompt。这种是浏览器级别的弹窗Selenium可以直接接管# 点击某个触发alert的按钮 alert driver.switch_to.alert print(alert.text) alert.accept() # 确认 # alert.dismiss() # 取消对于confirm弹窗需要点取消的就用dismiss()对于prompt弹窗可以在accept之前先输入内容alert driver.switch_to.alert alert.send_keys(输入的内容) alert.accept()这里踩坑最多的地方在于弹窗出现后如果不处理操作其他元素会报UnexpectedAlertPresentException。所以一般要配合显式等待确认弹窗出现了再去switch。第二类页面自定义弹窗div弹窗。这是网页自己用HTMLCSS模拟出来的本质就是普通元素。直接用元素定位点击关闭按钮或者确认按钮就行dialog wait.until(EC.visibility_of_element_located((By.CLASS_NAME, el-dialog))) dialog.find_element(By.CSS_SELECTOR, button.confirm).click()3.6 窗口切换与iframe做自动化测试窗口和iframe的切换是你迟早要面对的。多窗口切换点击一个链接打开新标签页这时候Selenium还在原来的窗口是不操作不了新页面内容的。需要先获取窗口句柄再切换# 保存初始窗口句柄 main_window driver.current_window_handle # 点击链接触发新窗口 driver.find_element(By.LINK_TEXT, 打开新页面).click() # 等待新窗口出现然后切换 windows driver.window_handles for handle in windows: if handle ! main_window: driver.switch_to.window(handle) break # 操作完新窗口后切回 driver.switch_to.window(main_window)iframe切换如果页面上有嵌入的iframe直接定位里面的元素是找不到的必须先切进去iframe driver.find_element(By.CSS_SELECTOR, iframe[srcform.html]) driver.switch_to.frame(iframe) # 操作iframe里面的元素 driver.find_element(By.ID, username).send_keys(test) # 切回默认内容 driver.switch_to.default_content()3.7 执行JavaScriptSelenium还提供了execute_script()方法这个能力非常强大。很多情况下页面元素的显示状态、滚动位置、属性值用常规API操作起来很别扭但是用JavaScript一行就解决了。比如把页面滚动到底部driver.execute_script(window.scrollTo(0, document.body.scrollHeight);)比如修改元素的value属性driver.execute_script(arguments[0].value 设定值, element)又比如用JavaScript直接点击某些被遮挡的元素driver.execute_script(arguments[0].click();, element)最后一个我经常用来兜底当某个按钮被遮罩层挡住element.click()会报ElementClickInterceptedException用JavaScript执行点击可以绕过去。不过注意这只是绕过问题不算真正解决问题。如果元素真的被遮挡导致用户点击不了那这是真实的前端bug测试不应该用它来掩盖问题。我的建议是先用它验证流程是否通畅回头还是要报给前端同学修复。3.8 截图与调试脚本失败的时候最有效的调试信息不是一行报错而是一张截图。Selenium里截图很简单driver.save_screenshot(screenshot.png)更实用的是只在失败的时候截图。可以在异常处理里加上from datetime import datetime try: driver.find_element(By.ID, save-btn).click() except Exception as e: timestamp datetime.now().strftime(%Y%m%d_%H%M%S) driver.save_screenshot(flogs/fail_{timestamp}.png) print(f元素点击失败: {e}) raise我还习惯在截图的同时打印当前页面的URL方便定位到底是哪个页面出了问题。另外如果你要定位脚本卡在哪一步可以配合driver.page_source把当时的HTML保存下来跟截图配合着看基本上什么问题都能查清楚。4. 和pytest一起组成完整的自动化测试项目单写几个脚本没什么真正能落地的是把它组织成一个可维护的测试项目。这里面pytest是目前用得最多的框架没有之一。4.1 pytest好在哪pytest和unittest比优势很明显断言简单直接用assert夹具fixture机制非常灵活插件生态丰富跟Allure报告无缝集成支持参数化数据驱动写起来很丝滑失败用例自动重跑、指定用例运行这些常用功能都有现成的插件。如果从零开始搭项目直接用pytest不用纠结。4.2 fixture管理浏览器实例写自动化最忌讳每个用例都新建一个浏览器、关掉一个浏览器那太慢了。正确的做法是用fixture来统一管理driver的生命周期。import pytest from selenium import webdriver pytest.fixture(scopefunction) def driver(): options webdriver.ChromeOptions() options.add_argument(--window-size1920,1080) driver webdriver.Chrome(optionsoptions) driver.implicitly_wait(5) yield driver driver.quit()然后测试函数直接接收这个fixturedef test_login_success(driver): driver.get(http://example.com/login) driver.find_element(By.ID, username).send_keys(tester01) driver.find_element(By.ID, password).send_keys(abc123) driver.find_element(By.ID, login-btn).click() assert 欢迎 in driver.page_sourcescopefunction表示每个测试用例都启一个浏览器用例之间隔离互不影响。如果一套流程要串多个用例可以把scope改成class或者module同一个浏览器跑完一组操作速度会快很多。4.3 数据驱动测试同一套测试逻辑要覆盖多组输入数据不用复制粘贴用例直接参数化import pytest pytest.mark.parametrize(username,password,expected, [ (tester01, abc123, 登录成功), (tester02, wrong_password, 用户名或密码错误), (, , 请输入用户名), ]) def test_login_with_params(driver, username, password, expected): driver.get(http://example.com/login) driver.find_element(By.ID, username).send_keys(username) driver.find_element(By.ID, password).send_keys(password) driver.find_element(By.ID, login-btn).click() assert expected in driver.page_source这样一条用例就能跑三个场景数据维护起来也方便。数据量大的时候还可以从Excel、CSV、YAML文件读取让测试数据跟代码分离。4.4 日志与Allure报告自动化跑完了一份清晰的报告非常重要。Allure是行业里用得最多的报告框架支持从pytest生成漂亮的可视化报告。pip install allure-pytest运行测试pytest --alluredir./test_result ./test_cases生成报告allure generate ./test_result -o ./allure_report --clean然后浏览器打开报告allure open ./allure_report报告里还能加测试步骤描述、附加截图import allure allure.step(输入用户名) def input_username(driver, username): driver.find_element(By.ID, username).send_keys(username) allure.step(点击登录按钮) def click_login(driver): driver.find_element(By.ID, login-btn).click()配上截图和步骤给领导看、给开发定位问题都直观多了。5. 常见问题排查速查表写了这么多年自动化把最常遇到的问题和排查思路整理成一张表照着查就行异常信息可能原因处理方法NoSuchElementException元素定位不到先确认页面是否加载完成再检查定位方式是否匹配当前页面结构用显式等待代替直接查找打开浏览器DevTools确认元素属性ElementNotInteractableException元素不可交互元素可能是隐藏的、被遮挡的或还没渲染完成等待元素可点击后再操作必要时用JS修改样式StaleElementReferenceException页面结构刷新页面可能在操作间隙重新渲染导致原元素失效重新定位元素后再操作不要缓存旧元素引用跨步骤使用ElementClickInterceptedException点击被拦截有弹层或遮罩挡在元素上面先关闭弹窗或用JS直接点击兜底TimeoutException等待超时确认等待条件是否写对确认页面是否真的加载了目标元素适当延长超时时间同时审视选择器ConnectionResetError/MaxRetryError网络波动加异常重试机制检查被测服务是否稳定排查是否有风控策略拦截了自动化访问再补一个开发环境相关的建议跑自动化脚本之前把浏览器上跟自动化无关的插件关掉特别是广告拦截类、密码管理类插件。这些插件会注入额外元素改变页面布局导致定位失败或者点击被拦截。很多人排查半天找不到原因最后发现是插件在捣乱。另外一个容易被忽略的是浏览器自动更新。不少自动化脚本跑着跑着就报driver版本错误十有八九是Chrome自动更新了但你的ChromeDriver没跟上。要么关掉浏览器的自动更新要么用一个稳定的Chrome版本配置文件锁住版本不升。6. 进阶思路往工程化方向优化基础的Selenium脚本人人都能写真正的差距在工程化能力上。第一个方向是把自动化测试接入CI/CD流水线。在GitLab CI或者Jenkins里配置一个自动化测试的任务每次代码合并到主干分支后自动触发测试脚本跑完自动把Allure报告推送到服务器上。开发提交代码后几分钟测试结果已经在推送通知里了这种效率提升是手工测试完全给不了的。第二个方向是失败重试机制的编写。真实项目里有的用例失败是因为偶发的网络抖动或者环境问题并不是产品真的有bug。这时候加一个重试机制能很大程度减少误报。pytest有现成插件pytest-rerunfailurespip install pytest-rerunfailures运行的时候指定重试次数pytest --reruns 2 --reruns-delay 1 ./test_cases第三个方向是用例分层设计。别把所有脚本堆在一个目录里往下摞更好的做法是分层Pages/层封装每个页面的元素和操作方法TestCases/层写业务场景和断言Data/层存放测试数据和配置文件。这样前端改版了只需要改Pages层测试代码主体基本不用动。这个思想叫Page Object ModelPOM做Web自动化测试绕不开也是面试必问的考点。第四个方向是AI辅助写脚本。最近有个趋势用AI生成Selenium脚本的效率越来越高。让大模型根据你的网页描述或者HTML片段生成定位代码然后你再人工修正、补充断言开发用例的总体时间能省掉不少。但我的经验是AI生成的代码只能当底稿定位策略和等待逻辑需要人工review。AI不知道哪些元素在真实场景里会变化盲信它生成的定位代码反而会埋下不稳定的隐患。我在实际项目里的习惯是让AI帮我搭骨架把页面元素、期望交互列清楚AI生成初版代码我再基于通用稳定性原则把显式等待和异常处理补上。这样两边的优势都有了整体效率能提升不少。最后再分享一个我个人的经验自动化测试不是为了把手工测试全替换掉而是把重复劳动交给脚本把人解放出来去做更有价值的探索性测试和复杂场景设计。我刚入行的时候也天真地以为自动化搞起来之后测试人员就没活干了干得越久越明白自动化只是一个工具它能帮你把路铺好但哪里需要挖深、哪里需要绕行这还得靠人来判断。踩过的坑多了你自然就会在写每条用例之前多想一步这个定位到底稳不稳这个等待条件会不会失效这个元素跨页面操作会不会变成stale element。把这些习惯养成之后你写出来的自动化脚本质量会高一大截。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进