Python Selenium Web自动化测试实战:从环境搭建到pytest集成
如果你每天点开同一个网站重复点那几个按钮、填那几个表单、验证那几行数据点得手指都快抽筋了那这篇文章就是为你准备的。用Python结合Selenium做Web测试自动化是我这几年接触过性价比最高的提效手段之一。只要把点击、输入、断言这些枯燥动作从人肉里剥离开交给脚本去跑你就能腾出手来做点更有价值的事甚至可以在下班前把回归测试全部跑完第二天上班直接看报告。这个方案的适用面很广。专职测试工程师可以用它做回归测试的护城河开发人员可以用它验证核心流程没有因为一次重构被改坏运维和数据分析师也能拿它做定时巡检和数据抓取。只要你面对的是浏览器里的网页操作且这套操作是重复性的Selenium基本都能接管。我后面会从一个完整可落地的角度把环境准备、脚本设计、运行框架到问题排查整个过程串起来讲尽量让照着做的人少踩几个坑。1. 内容整体设计与思路拆解1.1 为什么选择Selenium而不是其他方案不少人一上来就纠结工具选型。市面上做Web自动化的选择确实不少有主打轻量的Playwright有主打速度的Cypress也有老牌稳重的Selenium。我个人的观点是Selenium依然是兼容性最广、社区案例最丰富、新手入门最不容易卡壳的方案。先说兼容性。Selenium通过WebDriver协议驱动真实浏览器这意味着它模拟的是真实用户在操作浏览器的整个过程包括浏览器指纹、JS执行、Cookie维持等都是真实环境里的行为。对绝大多数业务系统来说这已经足够接近人工操作了。有些方案用的是自带浏览器内核虽然跑得快但遇到需要调用本机加密模块、U盾、依赖特定浏览器版本插件的系统时就会暴露出兼容短板。Selenium支持Chrome、Firefox、Edge、Safari全线主流浏览器这就意味着你写一套用例换一个浏览器环境改一行配置就能跑。再说学习曲线。Selenium的API设计非常直白driver.find_element去拿元素element.click()去点element.send_keys()去填逻辑跟人的操作习惯几乎一一对应。而Playwright虽然提供了更现代的自动等待和上下文管理但它引入了不少概念对没接触过自动化的人反而多了一层理解成本。如果你第一套自动化脚本打算亲自写完、亲自跑通Selenium的直白风格会让你少很多挫败感。最后是社区深度。Selenium存在的时间足够长你遇到过的卡点绝大多数都有人在Stack Overflow上问过、答过。这个意义比想象中大——自动化测试出错时最折磨人的往往不是被测系统而是坏境本身驱动版本不匹配、浏览器自动升级、元素定位失效。能快速搜到对症的解决方案就是生产力。1.2 自动化脚本的完整链路设计一套能稳定复用的Selenium脚本不是简单把点击步骤录下来就完了。我的做法是先画一条完整链路环境准备、用例设计、元素定位策略、等待策略、断言方式、报告输出、定时调度。环境准备是最容易忽视却最影响体验的一环。Chrome升级了ChromeDriver没跟上脚本就会当场罢工。后面我会给出固定版本组合的做法让环境相对封闭。用例设计上我倾向于把每个业务流程拆成一个独立测试用例比如登录-创建订单-查看列表-修改-删除这样一个链路拆成5条用例而不是一个大脚本从头跑到尾。这样每条用例可以独立执行、失败能定位、跑挂了一环不影响其他环节。元素定位是脚本稳定性的核心。我的优先级排序是ID属性 name属性 XPath相对路径 CSS选择器 链接文本。ID是元素的身份证只要开发没改ID脚本就不会断而XPath和CSS对页面结构调整比较敏感属于不得已才用的方案。这个优先级在用例设计阶段就要想清楚不要写脚本时临时拍脑袋。等待策略这块新手最大的误区是固定sleep几秒脚本跑得快还好跑得慢就瞬间变成定时爆炸。我一般滑动使用显式等待WebDriverWait配合expected_conditions里预置的条件比如元素可见、可点击、文本出现。这样脚本会在条件满足时立刻继续而不是死等一个固定时间。关于这部分细节后面第3章我会给出具体代码示例。断言和报告决定了脚本的结果是否可信。断言重在使用框架自带的assert机制而不是用print看一眼打印出来的颜色不会替你判断对错。报告我习惯用pytest配合pytest-html生成一份网页版执行结果谁跑挂了一目了然。至于定时调度Windows上用任务计划程序调用命令Linux上用crontab半小时配置一次就能每天自动回归。1.3 关键词对应的常见开发场景映射结合热搜词看的几个场景类型可以做一次映射方便你理解自己手上的需求该用哪一部分技术场景需求技术落点难度日常回归测试pytest Selenium 定时任务中批量表单填写提交Selenium元素操作 数据驱动低页面数据抽取元素定位 等待 CSV/Excel输出中接口联调前的页面预验证Selenium冒烟 requests接口校验高页面性能初步观察借助Network日志和耗时打点中把这些映射关系过一遍你会发现Selenium的定位不是银弹但在Web层自动化这个象限里它确实是覆盖最广的那把多用途刀。2. 核心细节解析与实操要点2.1 元素定位的黄金优先级与写法参考定位元素的策略直接决定了脚本能用多久。这是我踩了无数坑之后总结出来的选型逻辑你拿去做参考基本不会翻车。ID定位是首选不是因为它快而是因为它稳。绝大多数前端框架生成页面时重要的可交互元素都会带一个唯一ID。写法很简单driver.find_element(By.ID, username)实际项目里并非所有元素都有ID尤其是一些后端渲染的老系统ID全是动态生成的这时候退而求其次用name。很多表单控件有name属性语义明确且极少变化。driver.find_element(By.NAME, password)ID和name都拿不到时才考虑XPath。XPath里我强烈建议用相对路径配合文本和属性来定位而不是从根节点写一长串绝对路径。绝对路径对DOM结构调整几乎零容忍改一层所有脚本瞬时全废。# 相对XPath定位包含提交文本的button driver.find_element(By.XPATH, //button[contains(text(), 提交)])CSS选择器适合定位带特定class或属性组合的元素语法比XPath简洁在class含空格或层级复杂时表现更好。driver.find_element(By.CSS_SELECTOR, div.card button.primary)基本定位写法都在这儿了你可以对照自己的页面元素挑一种最稳的。当你的页面里有iframe时一定要记得先切进iframe再定位不然定位永远超时这个问题我后面还会细说。2.2 等待策略的三种姿势与适用场景初学的脚本十有八九挂在元素加载不出来。慢的接口返回3秒快的返回300毫秒固定sleep是把两者一刀切要么浪费大量时间要么直接超时跑挂。我目前使用的主方案是显式等待个别场景再补隐式等待兜底。先看显式等待的标准写法from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC element WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, submit_btn)) ) element.click()这个写法的好处是轮询检测不用自己写最大等待10秒元素一旦可点击立刻返回不浪费一毫秒。除了element_to_be_clickable我常用还有presence_of_element_located仅仅存在不要求可见和visibility_of_element_located可见才继续。隐式等待是用来兜底的。它设置的是轮询全局超时时间作用域是当前driver实例的所有查找操作。我通常把它跟显式等待搭配起到一个保底效果driver.implicitly_wait(5)这里要特别提醒一个新手常犯的错误不要把隐式等待和显式等待交叉使用在同一个查找逻辑里当两者同时生效时超时时间可能会叠加到最大值反而让失败用例等待时间翻倍。我的习惯是定义driver实例时只设置一次隐式等待后期对单个元素做精确控制时才用显式等待。真的要禁用显式等待时可以设置一个相对宽松的sleep兜底比如前置操作会触发文件下载、跨域跳转这类不可控流程import time time.sleep(2)sleep不是不能用而是要用在该用的地方。比如提交表单后页面跳转加数据回填这种流程就适合短sleep。稳定的等待策略才是脚本稳定性的防线它比任何定位技巧都重要。2.3 浏览器配置中的反爬规避与性能取舍用Selenium跑自动化时经常遇到网站的风控拦截。不少热搜词里也提到selenium 反爬虫说明这是高频卡点。平心静气说一句Selenium的自动化特征确实能被页面识别比如navigator.webdriver属性默认是truewindow.chrome对象和正常浏览器不一致等。碰到这类场景我建议的合规做法是只对你拥有权限或公开内容做自动化验证不要绕过任何登录验证码或风控机制。从配置层面优化浏览器参数是改善自动化脚本稳定性本身的合理行为三者的区别在于你做的是技术优化还是对抗行为。如果你只是做测试这些配置通常都不会被触发风控可以放心使用。以下配置是我在跑自有系统时常用的用无头模式减少资源占用并关闭一些不必要的功能from selenium.webdriver.chrome.options import Options chrome_options Options() chrome_options.add_argument(--headlessnew) chrome_options.add_argument(--disable-gpu) chrome_options.add_argument(--no-sandbox) chrome_options.add_argument(--window-size1920,1080)无头模式对回归测试特别适合——不需要弹窗干扰其他工作跑起来像静默巡更。但要注意无头模式与有头模式在渲染上可能存在差异我一般先用有头模式调试通过确认无问题后再切换到无头运行。此外还有两个性能相关的配置值得提一下一是当页面图片资源很大时可以通过--blink-settingsimagesEnabledfalse禁掉图片加载大幅提升加载速度二是如果跑的场景完全不需要CSS但需要JS可以动态拦截不必要的请求。但这两个设置都属于性能优化手段在使用上要确保不影响你的断言逻辑。2.4 日志与失败截图的双保险机制脚本挂了不可怕可怕的是挂了你不知道怎么挂的。我强烈建议在每一条用例里都加上失败截图和关键步骤日志。Selenium自带的失败截图接口很好用from selenium import webdriver from datetime import datetime driver.save_screenshot(ffail_{datetime.now().strftime(%Y%m%d_%H%M%S)}.png)可以把这个动作封装到一个自定义的异常处理装饰器里或用pytest的钩子捕捉失败用例自动截图。脚本里定期输出日志也是排查问题的重要线索import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logging.info(正在定位用户名输入框)日志可以记录当前操作到哪一步了故障现场还原起来非常快。实测下来有了截图和日志这两件事排查脚本失败的效率基本翻倍。3. 实操过程与核心环节实现3.1 从零搭建一套可运行的Selenium环境搭环境要的就是稳版本不对后面全是眼泪。我先给一套在Windows和Linux上都验证过可跑的方案你照着做基本不会出问题。第一步是确认Python环境。我推荐直接用python官网的安装包而不是用各类全家桶。安装时记得勾选“Add Python to PATH”这一步不做后面命令行输入python大概率直接报“不是内部或外部命令”。安装完成后打开终端验证python --version能正常打印出版本号说明Python已经就绪。第二步是准备虚拟环境这是保证项目依赖隔离的必要手段防止装一个包污染其他项目python -m venv venv # Windows venv\Scripts\activate # Linux / macOS source venv/bin/activate第三步是安装Selenium库和pytest框架。既然热搜词里就有pytest说明这个搭配确实是主流用法pip install selenium pytest pytest-html第四步是处理浏览器驱动。这是新手卡壳率最高的环节。Chrome的版本和ChromeDriver的版本必须严格对应差一位小版本号都可能启动失败。先打开Chrome的“关于”页面看版本号然后去ChromeDriver官网下载对应版本把下载的driver文件放进一个固定目录并把这个目录加到系统PATH环境变量里。或者更省事的方式是直接用webdriver-manager这个自动管理驱动的库pip install webdriver-manager之后代码里这么写驱动版本的检查和下载全部自动完成from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service driver webdriver.Chrome(serviceService(ChromeDriverManager().install()))建议新手第一次跑时先不要加无头模式让浏览器弹出窗口这样可以直观看到自己的每一步操作排查问题会容易很多。等脚本稳定后再切无头。3.2 一个能直接抄的Pytest测试用例模板接下来我给出一个完整的可运行示例。这个示例做一个简单的登录功能测试前后端逻辑都是本地的完全合规适合你快速跑通从启动到出报告的全过程。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 selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager pytest.fixture(scopefunction) def driver(): service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice) driver.implicitly_wait(5) driver.maximize_window() yield driver driver.quit() def test_login(driver): driver.get(https://your-site.com/login) username_input WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, username)) ) username_input.send_keys(testuser) password_input driver.find_element(By.ID, password) password_input.send_keys(testpass123) submit_btn driver.find_element(By.CLASS_NAME, submit) submit_btn.click() dashboard WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, //h1[contains(text(), Dashboard)])) ) assert dashboard.text Dashboard这个模板里把fixture作为每个用例的资源管理器测试结束自动quit浏览器不会残留僵尸进程。用例里用到的等待全部是显式等待避免网络波动下的时序错误。断言用pytest自带的assert失败了会生成清晰的对比信息。跑测试只需在项目目录下执行pytest -v --htmlreport.html执行完会得到一份report.html打开即可在浏览器中查看每个用例的执行状态。这套结构看起来简单但作为自动化测试的骨架它已经能承载一个中型项目的日常回归了。3.3 pytest数据驱动与参数化处理批量场景很多测试场景不是只测一组数据而是同一流程换不同数据反复执行。如果每组数据复制一份用例后期维护会让人崩溃。pytest的parametrize装饰器能优雅解决这个问题import pytest pytest.mark.parametrize(username,password,expect, [ (admin, 123456, 登录成功), (user1, wrong, 用户名或密码错误), ]) def test_login_with_data(driver, username, password, expect): driver.get(https://your-site.com/login) driver.find_element(By.ID, username).send_keys(username) driver.find_element(By.ID, password).send_keys(password) driver.find_element(By.CLASS_NAME, submit).click() result WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, message)) ).text assert expect in result这样一条用例就可以覆盖同一流程的多组输入和预期。实际项目中我还会把参数化数据单独抽到JSON或YAML文件里用pytest的pytest_generate_tests钩子动态加载实现测试数据与代码分离。数据文件坏了不会改代码业务侧改动数据也完全不用碰脚本这对长期维护意义非凡。3.4 定时调度与无人值守跑批的配置要点脚本写好了手动执行只能叫自动化了一半真正的价值在于到点自动跑。Windows和Linux的配置方向不同我单独说。Windows上打开“任务计划程序”创建基本任务。触发器选每天某个具体时间操作选“启动程序”程序填的是python解释器的绝对路径参数填脚本文件或pytest命令起始于目录填项目所在的绝对路径。这里有个关键细节不要图省事直接填pytest任务计划程序可能找不到这个命令。正确做法是填虚拟环境里的python.exe绝对路径并把-m pytest作为参数传进去例如D:\project\venv\Scripts\python.exe -m pytest -v --htmlreport.htmlLinux上用crontab配置定时任务0 9 * * * cd /home/user/project /home/user/project/venv/bin/python -m pytest -q --htmlreport.html几个隐藏点值得提醒一是运行环境要考虑PATH变量cron环境下可能和终端环境不同最好在脚本里用绝对路径引用driver二是日志输出要重定向到文件不然运行时产生的输出没有地方可看三是如果跑批涉及浏览器弹窗用无头模式更稳妥。4. 常见问题与排查技巧实录4.1 WebDriver与浏览器版本不匹配的处理这个问题遇到的频率有多高呢隔三差五就能在测试群看到有人问。Chrome自动更新是默认开启的今天还是124版本明天自动变成了125但本地的ChromeDriver还是124脚本启动时直接报session not created。解决思路有两个方向。第一个方案是借助webdriver-manager自动匹配版本大部分场景能解决但注意它在某些内网环境或代理环境下会因访问不了驱动下载源而失败。第二个方案是项目初始化时固定浏览器版本关闭浏览器自动更新同时在配置文件中明确记录driver和浏览器的版本组合。对严肃的测试项目我倾向于后者环境固定是稳定性的前提。版本一旦变了升级时先小范围验证再全面切新版本不要被动跟着浏览器的节奏走。4.2 元素明明存在却定位失败的原因排查页面元素在肉眼可见脚本却报找不到元素。这类问题排起来并不难按照下面的顺序逐层排查就好。先看是不是在iframe里。iframe是页面内嵌的独立文档主文档的查找逻辑对它无效。处理方式是在操作iframe内的元素前先切换上下文driver.switch_to.frame(frame_name) # 操作完切回主文档 driver.switch_to.default_content()再看是不是元素在Shadow DOM里这类元素要用JavaScript执行路径去拿普通find方法碰不到。再看是不是元素被遮挡或不可交互。常见场景是弹层遮住了按钮此时要定位并关闭弹层后再操作。显式等待里用element_to_be_clickable比presence_of_element_located更贴近真实操作的条件。再确认是不是定位条件不唯一页面存在多个相同属性元素。用find_elements返回列表选择符合预期下标或进一步细化XPath条件。4.3 脚本运行缓慢的提速思路回归测试用例一多运行半小时起步这时候优化就真的能省出时间。我常用的优化手段有几类。第一是把单用例内的等待时间压到最低用显式等待替换长sleep这是最见效的优化点。第二是开启无头模式省去渲染弹窗的开销。第三是合理复用浏览器实例多个用例共用同一个session但前提是各用例之间没有状态耦合——不然一条挂掉会污染后续全部用例。第四是并行执行或用浏览器并发实例通过pytest-xdist可以做到多进程并行执行时间几乎线性缩短但对被测试系统的并发承载能力有要求。如果被测系统扛不住并发访问并行反而会制造大量假失败。4.4 脚本跑挂了如何快速定位失败现场最后再说一个关于调试效率的策略。新手最容易犯的错是脚本挂掉后只看堆栈信息然后看着报错行发呆。我的处理流程是这样的当用例失败系统自动捕获截图并保存HTML源码。分析问题时先看截图失败时的页面状态一眼就能看出来截图不够再看保存的HTML源码用文本编辑器搜关键控件状态最后才回看日志确定是哪一步触发异常。有这个流程大多数失败问题五分钟内就能定位。pytest-html报告里会自动带上失败的堆栈信息如果把失败截图路径作为附件嵌进去排查体验还会更好。这一步操作很简单自行封装一个pytest_runtest_makereport钩子函数把截图复制到报告目录就可以。根据我自己的体会Selenium自动化测试真正难的不是API调用而是对不稳定环境的预见性。元素等不到就等到它出现拿不到就换一种方式拿。这套工具真正的优势在于它能替你把所有重复的路径测试一遍把这部分时间省下来去专注真正需要人工判断的复杂场景。最后再分享一个小技巧不要把脚本写完就丢在一边不管了。每两周花一点时间跑一遍项目里最核心的10条用例顺手修一修被页面改版影响的定位这套用例就会越用越顺手。日常跑批靠定时任务自己跑着偶尔回来看一眼报告这种省心程度用过的人都回不去了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →