ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ChromeDriver与IEDriverServer实战:配置与踩坑指南

ChromeDriver与IEDriverServer实战:配置与踩坑指南

先交代一个背景:我最早开始碰自动化测试的时候,用的还是Selenium 1时代的Remote Control,那会儿跑一条用例能折腾一整天,定位元素全靠XPath硬写,跑挂了还得靠经验猜是超时还是脚本写得不对。后来Selenium WebDriver一出来,感觉整个人都解放了——同样是写自动化,思路完全不一样了。这篇文章想聊的就是WebDriver体系下两个最典型、也是我日常工作里用得最多的浏览器驱动:ChromeDriver和IEDriverServer。

为什么单独把这两个拎出来讲?因为它们的“性格”完全相反。ChromeDriver现在是整个自动驾驶测试生态里最活跃、迭代最快的驱动,新功能几乎总是先支持Chromium内核这头;而IE驱动则是一个“历史包袱”极重的存在,很多公司因为老系统的原因,还必须跑着IE 11,于是你不得不同时和这两种驱动打交道。再加上这俩驱动的配置方式、踩坑点、排查思路都有各自鲜明的特征,搞懂了它们,再去看Edge、Firefox,基本就是举一反三的事。

这篇内容适合谁?入门WebDriver但还没被ChromeDriver版本兼容问题“毒打”过的同学,以及那些被公司老系统绑在IE 11上、每天挣扎在元素定位和浏览器弹窗里的测试开发工程师。我会把配置细节、参数含义、常见报错和排查思路都拆开讲,尽量让你照着就能落地。

1. 上手前的认知准备:WebDriver到底在干嘛

1.1 理解WebDriver协议,才能理解后面的所有报错

很多人一开始接触Selenium,第一件事就是写代码:

from selenium import webdriver driver = webdriver.Chrome() driver.get("https://example.com")

代码跑通了还行,一旦飘红就懵了——什么SessionNotCreatedException、WebDriverException、ElementNotInteractableException,全是英文,看着就头大。你要是只知道“这么写能跑”,遇到报错就不知道怎么排查。所以我建议你先花十分钟搞清楚一个基本问题:WebDriver到底是怎么把命令变成浏览器动作的。

WebDriver的本质是一个基于HTTP的JSON Wire Protocol(现在新版叫W3C WebDriver Protocol)。你的测试脚本(比如Python里的selenium库)其实是一个客户端,它把driver.get("url")这样的命令打包成HTTP请求,发给一个监听在本地端口的服务。这个服务是谁?就是ChromeDriver或IEDriverServer。服务收到请求之后,再通过浏览器内部的原生自动化接口去操作真正的浏览器窗口、标签页、DOM树。浏览器执行完动作,再沿原路返回结果。

这个结构决定了三件事:

第一,你的脚本和驱动之间是“进程级”的通信,所以驱动进程被杀掉、端口被占用、驱动版本不对,都会导致脚本直接报错;

第二,浏览器崩溃或者没启动成功,驱动是感知不到的,它只会告诉你“我请求超时了”或者“session失效了”;

第三,为什么每个浏览器都要有个独立的驱动?因为每个浏览器的内部接口都不一样,Chrome暴露的是DevTools协议,IE走的是IE自身的COM接口,你不可能让一个驱动同时指挥两种浏览器。

理解了这一点,你就能明白为什么后文要反复强调“驱动版本要和浏览器版本匹配”,因为驱动本质上是一个适配层,浏览器升级了内部协议,老驱动自然就没法对话了。

1.2 为什么每个浏览器都要单独装一个“驱动”

这是新手问得最多的问题之一:我都装了Selenium了,为什么还要另外下载ChromeDriver?

你想象一个场景:你要同时操作一台Windows电脑上的Chrome和IE,跑一套跨浏览器兼容性测试。如果Selenium直接把所有浏览器操作都塞进一个库里,那就意味着Selenium得维护所有浏览器的所有版本的内部接口,这个工程量大到不现实。所以浏览器厂商自己维护驱动项目——谷歌做ChromeDriver,微软做IEDriverServer,Mozilla做geckodriver。Selenium只是提供一个统一的上层API,底层谁去执行,交给各个驱动。

这其实是个很聪明的分权策略。但副作用就是,驱动版本必须跟着浏览器版本走,而且Windows、Linux、macOS要分别下载对应平台的二进制。Chrome 115之前,你还能指望Selenium Manager帮你省点事,但老项目里这种自动管理没有普及,很多人还在用硬编码的方式配置driver路径。

我现在的操作习惯是:不管用什么浏览器,都先确认浏览器版本和驱动版本,然后再决定是写死路径还是用自动管理。你如果跳到后面的章节,会发现版本匹配这块我反复提了三次,因为它在所有Selenium实战问题里,占比奇高、报错最莫名其妙、新手最容易翻车。

2. 环境搭建:ChromeDriver与IEDriverServer的配置细节

2.1 ChromeDriver版本匹配:不是越新越好

先说ChromeDriver。它和Chrome浏览器的版本对应关系,有个规律性的说法:ChromeDriver的大版本号要和Chrome的大版本号一致。比如Chrome 120,就找ChromeDriver 120.x.x.x,小版本可以略有差异,但大版本必须对上。

这个规则在Chrome 115之后又有点变化。Google开始推行Chrome for Testing(CfT)这个概念,把“稳定版浏览器”和“专门用于自动化的测试浏览器”分开。所以你会发现,Chrome 115之后,ChromeDriver官网给出的下载列表不仅有版本号,还有对应的CfT下载链接。你要是用普通Chrome配ChromeDriver,大概率能跑,但偶尔会遇到“session created but the browser didn’t respond”这类让人摸不着头脑的问题。此时换用CfT版本反而稳定得多。

放一个我整理的对应关系表,方便你自查:

Chrome版本ChromeDriver版本建议备注
Chrome 120ChromeDriver 120.x匹配即可
Chrome 115ChromeDriver 115.x开始推荐使用CfT
Chrome 114及以下ChromeDriver 114.x旧规则,大版本一样就基本能用
Chrome Dev/Beta对应版本+最接近的Stable驱动不稳定,测试环境慎用

下载驱动之后,Python这边有三种常见用法:

# 方式1:把驱动放进系统PATH driver = webdriver.Chrome() # 方式2:显式指定驱动路径 driver = webdriver.Chrome(executable_path="/path/to/chromedriver") # 方式3:用Service类(Selenium 4推荐) from selenium.webdriver.chrome.service import Service service = Service(executable_path="/path/to/chromedriver") driver = webdriver.Chrome(service=service)

注意Selenium 4之后,executable_path参数在webdriver.Chrome()里被标记为废弃了,更规范的写法是用Service对象。但很多老教程还在用老写法,代码也能跑,就是控制台会打一行DeprecationWarning。不建议忽略这类警告,因为下一个大版本可能就真的移除了。

新手最容易犯的错是把驱动放在桌面或随便一个没访问权限的目录里,然后脚本启动时报权限错误。Windows下尤其容易出现“不允许访问”这类问题,解法很简单,把驱动放到一个单独的、无空格的路径下,比如D:\tools\drivers\。

2.2 IEDriverServer的两大配置坑

IE没有像Chrome那样清爽的跨平台驱动,IEDriverServer只有Windows版本。这一点倒不意外,因为IE本身就跑在Windows上。但它的配置坑比Chrome多得多,而且每个坑都有“历史成因”。

第一个坑是位宽匹配。IEDriverServer有32位和64位之分,但这里有个反直觉的现象:64位的Windows系统上,64位IE驱动反而可能在定位大页面时出现一些怪异行为。社区里普遍推荐在64位系统上使用32位的IEDriverServer,因为IE的原生自动化实现是基于32位COM的,用32位驱动更稳定。我自己测过几轮,确实是这样,64位驱动偶尔会出现“定位到元素但点击无效”的问题,换成32位就好了。

第二个坑是“增强保护模式”。IE 11默认开启了增强保护模式,这个模式会阻止IEDriverServer通过原生接口访问浏览器。你如果直接跑脚本,大概率报错说“Protected Mode settings are not the same for all zones”或者干脆连session建立都失败。最简单的处理:在IE的Internet选项 -> 安全标签页里,把“启用保护模式”的勾选状态调整到一致,要么全选上、要么全不选。

别笑,这个“全选或全不选”的设定就是IE的怪脾气之一。W3C WebDriver规范要求所有区域的安全设置一致,但IE把“是否启用保护模式”当成了区域安全设置的一部分。你如果有跨域访问的场景,比如登录页是HTTPS、资源走HTTP,四个区域的安全级别不一样,IE就会翻脸。

2.3 Selenium Manager:现代项目的省心方案

Selenium 4.6之后官方引入了Selenium Manager,它最大的作用是自动帮你处理驱动的下载和版本匹配。你在没有设置任何webdriver.chrome.driver的情况下调用webdriver.Chrome(),Selenium Manager会自己去查Chrome版本、下载对应的ChromeDriver、启动驱动进程。

这个功能出来后,我一度觉得写死路径的方式可以退休了。但用了一阵子发现,Selenium Manager在联网正常、公司网络没有源封锁的情况下确实很爽,可在内网离线环境里就直接栽了。所以我的建议是:

  • 个人开发、本地调试,尽管用Selenium Manager,省心;
  • CI/CD流水线、内网测试环境,还是显式管理驱动版本最靠谱,避免网络抖动导致构建失败;
  • 混合场景:本地开发用自动管理,跑CI时用固定版本驱动塞进镜像。

这个策略说白了就是:自动管理省心但不可控,手动管理可控但费手。两者没有绝对好坏,关键是看你的执行环境允许多大程度的“自动化”。

3. Chrome自动化:Options参数是最值得花时间的部分

3.1 常用ChromeOptions参数逐个过一遍

ChromeDriver支持非常丰富的启动参数,这些参数基本对应Chrome命令行开关。你可以在启动浏览器之前定制一堆行为,比如禁用GPU、关闭沙箱、设置窗口大小、伪装移动设备、关闭弹窗、忽略证书错误等等。

给你看一段带常用参数的示例:

from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--headless=new") # 新版无头模式 options.add_argument("--no-sandbox") # 在root用户/容器环境必加 options.add_argument("--disable-gpu") # 老参数,现在不是必须但无害 options.add_argument("--disable-dev-shm-usage") # 解决/dev/shm太小导致的崩溃 options.add_argument("--window-size=1920,1080") # 设置窗口大小 options.add_argument("--ignore-certificate-errors") # 跳过证书错误 options.add_argument("--disable-notifications") # 禁用通知弹窗 options.add_argument("--lang=zh-CN") # 设置浏览器语言 driver = webdriver.Chrome(options=options)

这里每个参数我都遇到过对应的实际问题,不是随便堆上去的:

--no-sandbox:在Linux容器里跑测试时,如果不用这个参数,Chrome经常启动即崩溃。原因是Chrome默认的沙箱需要特权权限,而大多数容器环境没有配置。加上它也许会在安全性上打折扣,但你是在可控的测试环境跑,问题不大。

--disable-dev-shm-usage:这是容器环境下Chrome崩溃的又一高发原因。默认的/dev/shm只有64MB,Chrome多标签一开就爆了。改成使用/tmp之后,空间宽裕了,崩溃率能降下来不少。

--disable-gpu:在无显示器环境里,老版Chrome不开GPU反而稳定。现在新版本里这个参数越来越不重要,但加上它不会有什么副作用,我就保留着。

--ignore-certificate-errors:测试环境里自签名证书太常见了,不加这个,HTTPS站点直接白屏。注意生产环境千万别用。

--disable-notifications:防止浏览器原生的通知弹窗挡住元素。这类弹窗特别讨厌,它不是页面内的元素,Selenium默认无法直接关掉。

--window-size:这个要重点说,很多前端页面没有做响应式适配,窗口一变小,菜单就折叠。你如果固定了窗口大小,定位元素的稳定性会高很多。

3.2 headless模式实战要点

Headless模式是自动化测试里最常用的运行方式,因为不需要真的弹出浏览器窗口,可以跑在服务器上。但真到了实操,很多同学发现同样的脚本,带界面的能跑,headless就挂了。

原因集中在三点:

第一,headless模式下浏览器窗口默认是800x600。你的脚本如果升级到1920x1080,但忘了设置窗口大小,页面渲染出来的布局就和预期不一样,可能某些元素只在宽屏下可见。所以headless模式下,务必显式设置--window-size=1920,1080。

第二,headless模式默认的User-Agent里带了“HeadlessChrome”字样。有些网站会针对这个特征返回不同的页面,甚至直接拒绝访问。解决办法是覆盖User-Agent:

options.add_argument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Kill all human-readable UA strings")

注意这里只是举个例子,真实环境里你需要从浏览器里复制一份完整的UA字符串,而不是手写一个简版。

第三,headless模式下,浏览器的下载行为会被当成默认下载,不会触发另存为对话框。如果你测试的是下载功能,得配置prefs来指定下载目录和关闭自动打开PDF:

prefs = { "download.default_directory": "/tmp/downloads", "plugins.always_open_pdf_externally": True, } options.add_experimental_option("prefs", prefs)

我遇到过几次headless模式下点击下载按钮没反应的案例,排查到最后都是下载配置没设好,文件其实下载到了默认临时目录,但脚本和测试报告里找不到,看着就像“点击无效”。

3.3 移动端模拟:一套脚本模拟多种设备

移动端兼容性测试如果没有真机,ChromeDriver的mobile emulation可以帮你省掉一大笔设备费用。它的原理不是简单地把窗口缩小,而是模拟移动设备的屏幕尺寸、像素密度、User-Agent,甚至触摸事件。

mobile_emulation = { "deviceName": "iPhone 12 Pro" } options.add_experimental_option("mobileEmulation", mobile_emulation) driver = webdriver.Chrome(options=options)

也可以按需自定义设备参数:

mobile_emulation = { "deviceMetrics": { "width": 390, "height": 844, "pixelRatio": 3.0 }, "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15" }

用deviceName的好处是方便,但不同Chrome版本对deviceName的支持列表不完全一样,有时指定的机型在你的Chrome版本里不识别。我自己的习惯是尽量用自定义参数,至少保证UA和屏幕尺寸是自己确认过的,而不是赌Chrome内置的机型数据。

移动模拟最常见的坑是页面里有hover交互、或者依赖click坐标的事件。模拟触摸端时,某些hover展开的菜单会失效,需要改成click先展开再点击子项。另外,如果你的被测站点是PC版页面而不是移动版,仅仅改UA是不够的,有的站点会检测屏幕宽度来判断是不是移动端,这个都取决于前端实现,测试的时候建议先用浏览器手动访问确认一下。

4. IE自动化:兼容性问题的集中爆发区

4.1 Protected Mode的四个区域设置

这个坑我在2.2节简单提过,但因为它真的特别容易让人崩溃,我在这里展开讲一下。

IE的安全区域分为四块:Internet、本地Intranet、受信任的站点、受限制的站点。IEDriverServer启动时会检查四个区域的Protected Mode设置是否一致。也就是说,你如果只把“Internet”区域的保护模式关了,另外三个区域没动,IEDriverServer照样会报错。

我见过一个特别“经典”的场景:同事在同事的电脑上跑脚本是好的,换到自己电脑上立刻提示Protected Mode不一致,检查了半天,发现是IE设置里“以管理员身份运行”的选项影响了区域设置,导致四块区域实际生效的配置不一致。

解决办法:打开IE -> Internet选项 -> 安全,把四个区域全部选上“启用保护模式”,或者全部取消,然后重启IE。注意是“四个区域全部统一”,哪怕某个区域你不打算访问,也必须保持一致,否则就是白调。

还有一点容易被忽略:调整完设置后,注册表里的FEATURE_BROWSER_EMULATION可能还残留着旧的模式,IE 11本身有些兼容性视图设置也会干扰。建议调整后重启系统和浏览器,别偷懒。

4.2 缩放比例问题与定位失败的关系

这个坑特别“隐蔽”:IE驱动要求浏览器缩放比例必须是100%,否则元素坐标就会偏移。你在代码里用find_element_by_xpath找到元素,然后click(),脚本不报错,但执行的点击位置却偏了——点到了别的元素上,或者点到空白区域。

为什么会这样?因为WebDriver是基于坐标去执行原生鼠标事件的。缩放比例不是100%时,IE的CSS像素和物理像素之间出现缩放换算,而WebDriver协议里的坐标是以CSS像素计的,IEDriverServer负责把坐标换算成系统屏幕坐标。一旦缩放比例不是100%,换算结果就会偏。

排查方法很简单:看IE页面右下角的状态栏,或者用JavaScript检查一下:

driver.execute_script("return window.devicePixelRatio;")

正常情况devicePixelRatio应该是1,如果大于1或小于1,说明缩放比例不是100%。

解决也简单:在桌面空白处右键 -> 显示设置 -> 缩放与布局,调成100%。或者让脚本在启动前用subprocess调用display命令去设置。不过现实中有些环境和远程桌面连在一起,缩放比例经常被系统重置。所以更稳妥的办法是不要依赖手动改,写个自动化前置检查,发现比例不对就报警或者自动调整。

4.3 IE特有的五个关键Capability

IE驱动支持一堆IE专属Capability,这些配置写不对,运行时的诡异问题一个接一个。列五个我常用的:

Capability名称作用推荐值
ignoreProtectedModeSettings跳过Protected Mode检查false,除非真的没法统一设置
ignoreZoomSetting跳过缩放比例检查false,生产环境别开,开了就是埋雷
requireWindowFocus强制IE窗口获取焦点后执行操作true,解决点击偶尔失效
enablePersistentHover是否启用持久hoverfalse,Windows 10上更稳定
ensureCleanSession每次启动清理会话状态true,避免上一个用例遗留的登录状态

需要特别说明的是requireWindowFocus。IE在没有获得焦点的情况下,某些鼠标事件根本不触发。你把浏览器窗口最小化,然后跑脚本点击按钮,按钮明明在DOM里能定位到,但就是没有反应。把requireWindowFocus设成true,驱动会尝试激活IE窗口,这能解决大部分“元素找到了但点不动”的问题。

但代价是,打开这个选项后,脚本执行期间你没法操作其他程序,因为IE窗口会不断抢焦点。如果你需要在测试机边上干活,那就会很痛苦。我一般是在CI机上开这个选项,本地开发调试时关掉。

4.4 在Windows下让IE自动化稳定运行的技巧

IE自动化和Chrome自动化最大的区别在于:Chrome的启动、退出、DOM操作基本都是标准化的,IE则严重依赖操作系统的窗口管理器。

我在实践中总结了一套相对稳定的运行条件,满足这些条件的项目,IE自动化的稳定性会明显提高:

第一,用独立专机跑IE自动化,不要同时跑其他GUI程序。Windows的窗口焦点机制决定了IE自动化需要窗口处于激活状态,如果你开着其他程序抢焦点,点击就会随机失效。

第二,关闭Windows的“自动隐藏任务栏”和“切换应用时动画效果”。这些视觉特性会干扰窗口管理器对窗口位置的计算。

第三,设置虚拟桌面分辨率为固定值,比如1600x900,不要用DPI缩放。Windows的DPI缩放一直是IE自动化的天敌,能关就关。

第四,IE窗口打开后别手动去动它。脚本跑到一半,你手贱去滚动一下IE页面,后面的坐标全乱。这个看似无厘头,实际上我踩过很多次。

5. 实战脚本与常见问题排查

5.1 一个完整的登录测试脚本示例

纸上谈兵半天,不如给一个能直接跑的脚本框架。下面这个示例我用Selenium 4的语法写,同时兼容Chrome和IE,通过参数切换浏览器类型:

import sys 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.options import Options as ChromeOptions from selenium.webdriver.ie.options import Options as IEOptions def create_driver(browser): if browser == "chrome": options = ChromeOptions() options.add_argument("--window-size=1920,1080") options.add_argument("--ignore-certificate-errors") options.add_argument("--disable-notifications") return webdriver.Chrome(options=options) elif browser == "ie": options = IEOptions() options.attach_to_edge_chrome = False # 明确使用传统IE caps = { "requireWindowFocus": True, "ignoreZoomSetting": False, "enablePersistentHover": False, "ensureCleanSession": True, } for key, value in caps.items(): options.set_capability(key, value) return webdriver.Ie(options=options) else: raise ValueError(f"不支持的浏览器: {browser}") def test_login(browser): driver = create_driver(browser) try: driver.get("http://your-test-site.com/login") wait = WebDriverWait(driver, 10) username = wait.until(EC.presence_of_element_located((By.NAME, "username"))) username.clear() username.send_keys("testuser") password = driver.find_element(By.NAME, "password") password.clear() password.send_keys("Test@123") driver.find_element(By.ID, "login-btn").click() welcome = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, "welcome-msg")) ) assert "欢迎" in welcome.text print(f"[{browser}] 登录测试通过") finally: driver.quit() if __name__ == "__main__": test_login(sys.argv[1] if len(sys.argv) > 1 else "chrome")

这个脚本虽然简单,但已经包括了:按浏览器创建driver、显式等待、断言、finally中退出driver。实际项目里,你还需要加上日志、失败截图、重试机制。脚本只是一个基础骨架,重点是养成一个习惯:所有driver的创建和退出都放在一个可管理的地方,不要散布在各处,否则用例一多,你就开始怀念这个看似多余的封装。

这里再插一句封装经验:强烈建议把create_driver封装成工厂函数,而不是写一堆全局driver变量。全局driver的问题在于,一条用例崩了之后,driver不会自动退出,下次再跑就会有一堆僵尸浏览器进程占用CPU和内存。用工厂+上下文管理器模式能很好地规避。

5.2 等待策略:让脚本从“碰运气”变“稳定”

脚本“有时候能跑,有时候不能跑”,十有八九是等待策略不对。Selenium里大致有三种等待方式:

  • 隐式等待(implicitly_wait):设置一个全局的轮询超时,driver.implicitly_wait(10)表示在10秒内,如果元素没有出现,会持续尝试查找,超时才抛异常。
  • 显式等待(WebDriverWait):针对单个条件,比如“等待按钮可点击”、“等待元素消失”、“等待URL包含某字符串”。
  • 强制等待(time.sleep):不推荐用来做业务逻辑,但某些特殊场景不得不用,比如依赖第三方动画或外部系统响应。

我的经验分配是:隐式等待设一个保底值,比如10秒,用于兜底;关键路径全部用显式等待;sleep尽量少用,只在极少数“等待动画播放完”的场景里用,而且给的时长要刻意调小,传个500毫秒,不要一上来就time.sleep(5)。

显式等待比隐式等待强在哪里?隐式等待只能控制“查找元素”的轮询,但“元素找到了”和“元素可用”是两回事。你等到了<a>标签出现,但它可能还是disabled状态,你需要的是“可点击”。这种条件,隐式等待完全管不了,只能用显式等待配合element_to_be_clickable。

wait = WebDriverWait(driver, 10) login_btn = wait.until(EC.element_to_be_clickable((By.ID, "login-btn"))) login_btn.click()

另外,显式等待的异常信息里通常会描述“等待了10秒、条件是什么、当前页面URL是什么”,排查问题比裸奔的NoSuchElementException直观太多了。

5.3 高频异常对照表

直接上干活:把最常见的异常和对应解法整理成一张表,下次你再看到报错,先翻这张表:

异常类型出现场景常见原因排查方向
SessionNotCreatedException启动浏览器时ChromeDriver与Chrome版本不匹配核对两个版本号
NoSuchWindowException切换窗口IE窗口被关闭或崩溃检查是否误点了关闭按钮
NoSuchElementException定位元素等待不够或元素不在DOM先加显式等待,再看选择器
ElementNotInteractableException点击/输入元素被遮住或只读检查CSS遮挡、换个操作方式
StaleElementReferenceException点击上一步找到的元素DOM被刷新,旧引用失效重新定位元素
TimeoutException等待条件条件没满足或等待超时确认页面是否真的触发相应变化
WebDriverException任意阶段驱动进程异常退出查driver日志
InvalidElementStateException输入元素不可编辑检查readonly属性

要特别说一下StaleElementReferenceException,这是新手最懵的异常之一。你明明上一步才找到的元素,下一步就报“引用已过期”。原因通常是这个元素所在DOM被重新渲染了一次,原来的对象被替换了。最典型的场景是:点击刷新按钮后,你还在用刷新前的元素引用。解法不复杂,就是每个动作之前重新定位元素,或者用wait.until里返回的一次性引用,别把引用存在变量里用太多次。

另一个要提醒的是ElementNotInteractableException,它和NoSuchElementException的排查方向完全不同——前者是“找到了但没法操作”,后者是“根本没找到”。很多人一报异常就以为定位器写错了,实际上可能是页面弹出遮罩层挡住了元素。用JavaScript去检查遮挡物是最快的:

driver.execute_script( "return document.elementFromPoint(arguments[0], arguments[1]);", element.rect["x"] + element.rect["width"] / 2, element.rect["y"] + element.rect["height"] / 2, )

打印返回的标签名,看看到底是什么挡了你一眼。

6. 工程化建议:让自动化测试可持续

6.1 浏览器驱动的统一管理与自动更新

自动化脚本写得再好,如果驱动版本跟不上浏览器升级,某天早晨你到公司发现所有用例“全军覆没”,肯定会崩溃。所以驱动管理一定要有“版本策略”。

我自己维护的驱动管理逻辑是这样:

第一,记录当前环境里的浏览器版本,找一个固定的基线版本。比如公司统一安装了Chrome 118,那么环境里ChromeDriver就用118.x,别在没人通知的情况下私自升级。

第二,驱动二进制放到一个统一的目录,环境变量PATH指向它。Windows上我习惯放D:\tools\SeleniumDrivers\,里面按浏览器分目录:

SeleniumDrivers\ ├─ chrome\ │ └─ chromedriver.exe ├─ ie\ │ ├─ 32\ │ │ └─ IEDriverServer.exe │ └─ 64\ │ └─ IEDriverServer.exe └─ README.md

第三,把版本号写进项目的配置文件,启动时检查一次。一旦发现浏览器版本和驱动版本不匹配,直接报警提示,而不是等脚本跑挂了再去翻日志。可以用一个非常简单的脚本实现:

import shutil import subprocess browser_version = subprocess.check_output( ["reg", "query", r"HKLM\SOFTWARE\Google\Chrome\BLBeacon", "/v", "version"], ).decode() # 这里只是示例,实际取版本号的方式各环境不同 driver_version = subprocess.check_output(["chromedriver", "--version"]).decode()

这种做法虽然“土”,但非常实用。尤其是在超过10人的测试团队里,每个人本地装的环境不一样,没有一个统一的版本约定,早晚出乱子。

6.2 失败截图与日志的落地

最后想给一个建议:在自动化测试里,截图和日志的价值往往被低估。你脚本跑挂了,只有一段红色的异常堆栈,排查效率极低。但如果你能在失败时自动把当前页面截图、把浏览器控制台日志、把DOM关键信息都保存下来,排查成本能降一个量级。

截图的做法很简单:

from selenium.webdriver.common.by import By from datetime import datetime import os try: # ... 业务动作 ... pass except Exception: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") screenshot_dir = "./screenshots" os.makedirs(screenshot_dir, exist_ok=True) driver.save_screenshot(f"{screenshot_dir}/failure_{timestamp}.png") with open(f"{screenshot_dir}/page_{timestamp}.html", "w", encoding="utf-8") as f: f.write(driver.page_source) raise

这里有几个细节值得注意:save_screenshot拿到的是当前视口内可见区域的截图,不是全页面截图。真要全页面截图,得用特殊脚本。还有,保存page_source时,如果页面是异步渲染的,拿到的HTML可能是加载完成前的状态,所以最好配合等待条件。

日志方面,Selenium驱动本身是会输出日志的,尤其是ChromeDriver,有--verbose参数。如果你遇到浏览器启动失败、崩溃这类疑难杂症,把ChromeDriver的日志加上:

service = Service( executable_path=chrome_driver_path, service_args=["--verbose", "--log-path=./logs/chromedriver.log"] ) driver = webdriver.Chrome(service=service)

这样浏览器内部发生了什么就一目了然了。

另外,日志和截图要按时间归档。别把所有失败截图都堆到一个目录里,久了之后分不清哪张对应哪次执行。我习惯按“日期/用例名”分层存储,一天一个目录,用例名前缀区分。

7. 写在最后的一点经验

Chrome和IE两个驱动,一个代表“向前看”,一个代表“向后兼容”。这几年我的体感是,Chrome自动化越来越简单,因为Chrome本身在稳定性和规范性上投入巨大,新版本对WebDriver的支持越来越好;而IE恰好相反,微软自己都在推Edge了,IE自动化的环境只会越来越“别扭”。如果你现在还在维护IE自动化,我的忠告是:尽量把用例隔离在一个小范围,别把IE跑全量的回归测试,因为IE的稳定性天花板摆在那里,你不可能靠优化代码突破它的系统限制。

等到你真的上手,用我上面说的方式检查了Protected Mode、修正了缩放比例、调好了等待策略,你会发现IE的很多“诡异”问题其实都有迹可循。它们不是随机发生,而是你的脚本和IE之间有一条没对齐的“暗线”——这条暗线可能就是某个Capability、某个系统设置。

最后,无论你们团队用的是哪一套测试框架,尽量把驱动管理、等待策略、异常处理做成公共模块,不要每个用例各写各的。自动化测试久了你就知道,用例数量增长不可怕,可怕的是连维护成本一起指数级增长。把底子打好,新旧浏览器混跑也能保持清晰的代码结构,这对长期维护来说,价值比任何一个单项优化都大。

返回列表