ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Selenium自动化测试实战:从WebDriver原理到可维护框架搭建

Selenium自动化测试实战:从WebDriver原理到可维护框架搭建 前阵子帮团队搭一套Web端回归测试的框架脚本跑得好好的某天忽然弹出一个滑块验证框整个流水线直接卡死。那几天我对Selenium的认知重新刷新了一遍它确实是个在自动化测试领域驰骋多年的老将但真正要在项目里用好它光会find_element远远不够。围绕Selenium 自动化测试这个命题我把从零搭建环境、处理元素定位、整合pytest、再到排查各种疑难杂症的实战路径完整梳理了一遍希望这篇内容能让你不只学会Selenium的API更能独立搭出一套能维护、能复用的自动化测试方案。Selenium能做什么、适合谁我先用一句话说清楚Web端UI自动化测试、回归测试、以及把高频重复的网页操作脚本化都绕不开它无论你是刚接触自动化测试的新人还是需要把手动回归流程转成自动化的测试工程师下面这些内容应该都能直接参考落地。1. Selenium 的核心逻辑WebDriver与真实浏览器的交互1.1 为什么UI自动化测试绕不开Selenium先聊一个基础问题做接口测试用requests或httpx就够了为什么UI层还要专门搞一套工具原因在于接口测试只能验证后端逻辑但用户最终接触的是页面。页面上的按钮能不能点、表单能不能提交、弹窗会不会挡住操作、数据渲染是否正常这些是接口层看不到的问题。Selenium的价值在于它直接驱动真实的浏览器模拟用户在页面上的操作轨迹让脚本和真人使用几乎站在同一视角去验证产品体验。对比同类工具Playwright和Cypress近年也很火但Selenium的生态积累最深、对多语言的支持最完整也是很多企业已有技术栈里最稳妥的选择。如果你要维护老项目或者需要在不同浏览器、不同节点上跑分布式回归Selenium依然是那个兜底能力最强的方案。1.2 WebDriver协议是怎么工作的理解Selenium先理解WebDriver。Selenium并不是用某种魔法远程控制浏览器而是通过一条标准协议来沟通你的测试脚本Python、Java、JS等作为客户端浏览器驱动比如ChromeDriver、GeckoDriver作为中间桥梁目标浏览器作为真正执行操作的对象这条中间桥梁就是WebDriver协议早期叫JSON Wire Protocol现在已经是W3C标准了。正因为有了这个标准你的测试代码才能在同一套API下驱动Chrome、Firefox、Edge等不同浏览器。这里有个初学者最容易忽略的坑浏览器驱动版本必须和浏览器主版本匹配。比如你的Chrome升级到了120但ChromeDriver还停留在117脚本启动时就会直接报SessionNotCreatedException连浏览器窗口都弹不出来。这个问题我在后面的翻车记录里会专门展开。1.3 Selenium家族的真实分工很多人以为Selenium就是一个库其实它有三件套Selenium WebDriver核心库也是绝大多数人日常用的自动化测试工具负责脚本化的浏览器操作Selenium IDE一个浏览器录制插件点点鼠标就能录下操作过程并导出脚本适合快速做原型验证Selenium Grid分布式执行服务可以把测试分发到多台机器、多个浏览器上并行跑我自己的使用建议是IDE用来快速验证需求还行团队实际测试工程里90%的工作都落在WebDriver pytest这套组合上Grid则是在用例数量上来之后为了缩短整体回归时间再考虑引入的组件。2. 搭一套稳妥的 Selenium pytest 测试环境2.1 版本选型Chrome与ChromeDriver必须对应这一节我直接给出一套经过多次验证的搭建步骤尤其适合还没有完整环境、想从零开始跑通的读者。我假设你的主力语言是Python因为它在测试领域的使用成本最低。环境依赖大致如下pip install selenium pytest pytest-html webdriver-manager这里我强烈建议用webdriver-manager它可以在启动时自动检查本机浏览器版本拉取对应的驱动省去手动管理ChromeDriver版本的痛苦。我早期不用它的时候每次Chrome一升级回归流水线就红成一片后来彻底换成自动管理才清净。如果你还是想手动下载驱动记住一个技巧先打开Chrome在地址栏输入chrome://version查看具体版本号再去ChromeDriver官网找相同大版本的驱动文件不要直接下载网页上最新版。2.2 依赖列表的管理建议团队协作时依赖版本不锁定就是定时炸弹。我习惯把环境依赖固定在一个requirements.txt或pyproject.toml里核心几项是selenium4.0 pytest7.0 pytest-html4.0 webdriver-manager4.0为什么要锁版本因为Selenium 4相比3代改动较大如果你在公司的老项目里用的是3.x网上很多新教程的API写法不一定兼容。先统一版本再谈功能落地。2.3 第一个能真正跑通的用例环境准备好之后我建议不要一上来就写复杂的业务脚本先写一个最小的用例确认整条链路通不通from selenium import webdriver from selenium.webdriver.chrome.options import Options def test_open_page(): options Options() options.add_argument(--headlessnew) # 无头模式适合快速验证 driver webdriver.Chrome(optionsoptions) driver.get(https://example.com) assert driver.title Example Domain driver.quit()先跑这个用例如果失败说明问题出在环境而非业务代码。等它绿了再逐步加复杂操作。提示--headlessnew是Chrome新无头模式比老无头模式更接近真实浏览器行为。如果不想用无头模式把这一行注释掉即可。3. 写测试脚本前必须搞懂的核心API细节3.1 元素定位八大方式与xpath实用选择Selenium八大定位方式分别是ID、NAME、CLASS_NAME、TAG_NAME、LINK_TEXT、PARTIAL_LINK_TEXT、XPATH、CSS_SELECTOR。日常我用得最多的就是ID和CSS选择器其次才是XPath。不同定位方式的稳定性差异很大我的经验排序是定位方式适用场景相对稳定性ID元素有唯一ID时极高CSS_SELECTOR结构清晰、带class或属性高XPATH没有稳定属性、需要用文本或位置中CLASS_NAMEclass比较独特时中LINK_TEXT定位超链接中TAG_NAME批量校验某些标签低一个真实案例页面上表单输入框的ID是username但另一个业务的输入框ID变成了动态生成的input_123456这类动态ID就不能靠ID定位。换成CSS选择器配合稳定的name属性和input[typetext]来定位更稳妥。XPath方面记住一句实用心法尽量少用绝对的/html/body/div[2]...路径多用相对路径和contains函数。比如driver.find_element(By.XPATH, //button[contains(text(), 立即登录)])这种方式即使页面结构发生了部分变化只要按钮文字不变脚本依然能找到。相比写死层级路径抗页面改版的能力强得多。3.2 等得对隐式等待、显式等待与sleep的本质区别新手写自动化测试最容易犯的错就是遇到元素加载慢就在代码里time.sleep(2)。用sleep不是不行但它有两个致命问题不管页面有没有加载完都要傻等多等了没意义的秒数网络一波动固定等两秒可能远远不够脚本照样报错。正确的做法是显式等待让它等到某个条件满足才继续执行from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def wait_for(driver, locator, timeout10): return WebDriverWait(driver, timeout).until( EC.visibility_of_element_located(locator) ) def test_login(driver): driver.get(https://example.com/login) wait_for(driver, (By.CSS_SELECTOR, #username)) driver.find_element(By.CSS_SELECTOR, #username).send_keys(tester)这段代码里WebDriverWait会轮询元素的可见状态最多等10秒条件满足就立刻继续不用浪费用例时间。对于提示文本、按钮可点击这类场景EC.text_to_be_present_in_element和EC.element_to_be_clickable同样好用。隐式等待driver.implicitly_wait(5)是全局设置作用是在元素找不到时轮询查找等待但它不会判断元素是否可点击、是否可见。我的一般做法是全局设一个较短的隐式等待兜底关键操作前用显式等待做精准保障。3.3 页面滚动纵向、横向以及滚动到元素可见网页左右滑动滚动可见这类关键词最近很热。平时我们最常处理的是纵向滚动直接把页面拉到浏览器底部driver.execute_script(window.scrollBy(0, document.body.scrollHeight))不过真正容易踩坑的是横向滚动。有些列表、表格或图表容器有自己内部的横向滚动条这时候很多人会默认滚window结果脚本报元素找不到或元素不可点击。横向滚动的正确姿势是先定位到可滚动的容器再在容器内滚动container driver.find_element(By.CSS_SELECTOR, .scroll-table-wrapper) driver.execute_script(arguments[0].scrollLeft 500, container)如果要滚动到某个元素正好出现在可视区域内最通用的办法是scrollIntoViewtarget driver.find_element(By.CSS_SELECTOR, .submit-btn) driver.execute_script(arguments[0].scrollIntoView({block: center}), target)block: center表示把元素滚动到屏幕中间位置比默认的start在很多弹层和固定导航栏的场景里更友好避免元素被顶部悬浮层挡住。3.4 窗口句柄与iframe切换点击一个链接后打开新窗口这是Selenium新手经常懵的地方。页面一多driver以为的当前页面还是旧窗口自然找不到元素。解决方法是用window_handles切换driver.find_element(By.LINK_TEXT, 新窗口链接).click() driver.switch_to.window(driver.window_handles[-1]) # 切到最新窗口iframe也是重灾区。网页里嵌入第三方登录、地图、支付组件时元素在iframe里直接find_element必然失败。必须先切进去driver.switch_to.frame(mainFrame) # 按iframe的id或name driver.find_element(By.CSS_SELECTOR, .login-btn).click() driver.switch_to.default_content() # 退出iframe回到主页面我见过很多同学卡在这里大半天其实核心就一句话操作前先想清楚这个元素在哪个DOM环境里窗口也一样先明确当前上下文在哪个窗口/哪个iframe。3.5 下拉框、文件上传与弹窗的典型操作这三个控件是业务测试里出现频率最高的。下拉框如果用的是原生select元素用Select类操作最稳from selenium.webdriver.support.ui import Select select Select(driver.find_element(By.CSS_SELECTOR, #city)) select.select_by_visible_text(上海)文件上传不用模拟点击控件很多界面上的选择文件按钮本质是一个input[typefile]直接用send_keys把本地文件路径传进去就行driver.find_element(By.CSS_SELECTOR, input[typefile]).send_keys(/path/to/test.csv)弹窗分两类浏览器原生alert可以switch_to.alert处理页面自研弹窗则只是普通HTML元素直接用正常定位操作。4. 从脚本到工程让Selenium用例真正能维护4.1 用pytest fixture管理浏览器生命周期写完几个散装脚本之后下一步就是工程化。pytest是Python测试领域的事实标准框架和Selenium搭配时最有价值的是fixture机制。在conftest.py里定义一个driver的fixture所有用例都可以自动复用import pytest from selenium import webdriver from selenium.webdriver.chrome.options import Options pytest.fixture def driver(): options Options() options.add_argument(--window-size1920,1080) driver webdriver.Chrome(optionsoptions) yield driver driver.quit()这个fixture会在每个用例执行前创建浏览器执行后自动关闭。好处是不用在每个用例开头结尾重复写webdriver.Chrome()和driver.quit()用例失败时也能保证浏览器被正确清理不会残留僵尸进程。如果说要共享同一个浏览器实例跑多条用例可以把fixture的scope改成module或者session。我实际使用下来的经验是登录类的公共前置步骤可以考虑复用会话但业务用例里尽量一个用例一个浏览器实例否则用例之间的缓存和登录态会互相污染出了问题极难排查。4.2 参数化与数据驱动从一条用例到一批用例测试登录框的时候你不能只测一组正确账号至少还得测几个典型错误账号。如果把这些情况都复制粘贴成独立函数代码会非常难看。pytest的parametrize装饰器可以把数据从用例逻辑里抽出来import pytest pytest.mark.parametrize(username,password,expected_tip, [ (plain_01, wrong_password, 账号或密码错误), (locked_user, any_password, 账号已被锁定), ]) def test_login_fail_cases(driver, username, password, expected_tip): driver.get(https://example.com/login) driver.find_element(By.CSS_SELECTOR, #username).send_keys(username) driver.find_element(By.CSS_SELECTOR, #password).send_keys(password) driver.find_element(By.CSS_SELECTOR, .submit-btn).click() tip driver.find_element(By.CSS_SELECTOR, .error-tip).text assert tip expected_tip后续如果新增一类错误账号只需要往参数列表里加一行数据即可测试逻辑本身完全不用动。这也符合数据驱动的基本理念测试代码只关心操作和断言而测试数据从外部传入。4.3 失败截图与HTML报告自动化测试的事故现场没有截图和报告的自动化测试等于白跑。脚本失败时最怕的就是人不在电脑前事后只能看到一个AssertionError。在conftest.py加一个失败截图钩子能在用例失败时自动保存现场截图pytest.hookimpl(tryfirstTrue, 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: timestamp time.time() driver.save_screenshot(freports/failure_{timestamp}.png)配合pytest-html运行完测试后执行命令pytest --htmlreports/report.html就能生成一份带失败截图链接的报告。这套组合是我在团队里推广的标配失败了能立刻定位是数据问题、环境问题还是断言问题而不是只丢一句报错信息。4.4 别把所有代码堆在一个函数里Page Object模式当用例规模超过50条你一定会遇到这个问题页面上某个输入框的定位方式从#username改成了#login-username结果全项目上一百个地方都在直接使用这个选择器你只能全局搜索替换改得战战兢兢。Page Object模式是解决这个问题的经典思路把页面元素和交互操作封装到独立的类里测试用例只关心业务场景不接触底层定位细节。class LoginPage: def __init__(self, driver): self.driver driver def input_username(self, username): self.driver.find_element(By.CSS_SELECTOR, #username).send_keys(username) def input_password(self, password): self.driver.find_element(By.CSS_SELECTOR, #password).send_keys(password) def click_login(self): self.driver.find_element(By.CSS_SELECTOR, .submit-btn).click() def get_error_tip(self): return self.driver.find_element(By.CSS_SELECTOR, .error-tip).text用例代码立刻变得可读def test_login_success(driver): page LoginPage(driver) page.input_username(tester) page.input_password(123456) page.click_login() assert 欢迎回来 in driver.page_source页面改动时只需要调整LoginPage内部的选择器所有用例都不受牵连。这个模式看起来简单却是工程化进阶里最值得先做的一件事。5. 实测里最容易翻车的场景与排查思路5.1 版本不匹配引发的Session异常场景描述脚本启动时浏览器窗口一闪而过控制台报错SessionNotCreatedException: This version of ChromeDriver only supports Chrome version 114 Current browser version is 120.0.6099.109这个报错信息已经把问题说得很明白了。解决方案也非常直接让webdriver-manager接管驱动版本。如果你坚持手动管理那就在团队的文档里写清楚升级浏览器前先同步升级驱动否则这个坑一定反复踩。5.2 自动化测试脚本被误判为爬虫这个场景我特别想多说两句因为最近关于Python Selenium和反爬虫机制的讨论很多。在合法合规的自动化测试中我们的脚本也偶尔会被一些风控系统误判为爬虫表现为页面莫名出现滑块验证登录后立刻掉线返回数据的格式和真人访问不一致我处理这类问题的思路首先不是去对抗风控而是先自查脚本本身是不是太像机器人了请求频率是否过快、扫码页面是否被反复刷新、User-Agent是否过于单一。自动化测试的目标是验证业务功能不是在别人站点上做批量数据采集所以我的原则是把脚本的访问节奏限制在真实用户的操作频率内不做任何越界行为。如果确实因为环境检测导致连正常的测试流程都无法完成也可以从浏览器指纹参数上做一些常规调整比如去掉自动化标识等但请明确这些手段只能在合法授权的测试环境和个人学习场景中使用绝不能用于规避网站正常的风控规则。5.3 元素明明存在但一直报NoSuchElementException这个报错估计每个用过Selenium的人都遇到过。排查顺序我总结如下先用driver.page_source或截图看看当前页面到底是什么状态有没有可能页面还停留在上一页检查元素是不是在iframe里检查元素是不是在新打开的窗口里检查元素是否需要滚动才能渲染很多列表是懒加载的不滚到底根本不出现检查定位方式本身是否写错尤其是XPath里的引号和层级排查定位问题时我经常直接在浏览器F12控制台验证$x(//button[contains(text(), 立即登录)])如果这条在控制台能定位到元素而在Selenium里定位不到问题基本可以锁定在等待、iframe或窗口上下文而不是定位表达式本身。这条经验帮我省下过无数瞎折腾的时间。5.4 网络抖动导致的超时与失败自动化测试跑在本地和跑在CI上的稳定性差异很大程度上来自网络环境。解决办法是对页面加载策略和超时做显式配置from selenium.webdriver.chrome.options import Options options Options() options.page_load_strategy eager # 不等所有资源加载完DOM就绪即可 driver.set_page_load_timeout(15) driver.set_script_timeout(10)eager这个策略对于图片较多、加载很慢的页面特别有用。比如一个后台管理页面有一大堆图表组件全套加载完可能要几十秒但核心操作按钮在DOM就绪时就能点了用eager可以大幅减少无用等待。6. 从桌面Web到移动AppSelenium生态的延伸思考6.1 Appium和WebDriver协议的关系热搜词里出现了appium自动化测试这里我简单梳理一下关系。Appium是针对移动端App的自动化测试框架它底层复用了WebDriver协议的思路通过driver中间层去驱动iOS和Android上的真机或模拟器。如果你已经熟悉Selenium的定位、等待和断言模式上手Appium的成本其实很低很多API是同一套设计思路。不同点主要是鼠标点击变成了触屏操作find_element之后多了滑动、长按、手势等动作元素定位在原生App里用resource-id、content-desc等属性所以我的建议是Web端的Selenium体系是你理解所有UI自动化测试的母语学透了它再看Appium、Playwright都会觉得非常熟悉。6.2 什么时候该引入Selenium Grid当用例数量超过300条单机串行跑完一次回归可能要两三个小时这时候就该考虑并行执行了。Selenium Grid的基本架构是一台Hub机器负责接收测试请求并分发任务多台Node机器分别注册到Hub每台Node可以配置不同的浏览器环境启动方式也不复杂早期版本需要单独下载selenium-server运行Selenium 4之后官方建议直接用相对简化的接入方式。不过这类并行改造有一个前提用例本身之间不能有强依赖用例A依赖用例B执行结果的顺序必须提前拆干净否则并行反而会让结果更乱。6.3 AI自动化测试Selenium会过时吗AI自动化测试成为热搜词确实反映了行业风向。现在有不少团队在尝试用大模型生成定位器、自动修复选择器失效问题、甚至自动生成断言逻辑。但我个人的判断是在可见的未来里AI更多是辅助工具减少测试工程师的重复劳动而不是彻底替代Selenium这一类自动化框架。真正的自动化测试核心从来不是某个工具而是你对业务场景的理解、对稳定性的把控、对问题排查的思路。工具会迭代但底层的能力不会过时。如果你也在从零搭自己的第一套Selenium测试工程我的建议很朴素先把一个完整的登录场景跑通再逐步加上等待、参数化、Page Object、失败截图。一步一步来这套东西没有你想象的那么难也没有你想象的那么简单但绝对值得你投入时间。
返回列表