做了这么多年自动化测试我踩过最深的一个坑就是用例写得挺顺一到回归就抓瞎。大几小时跑不完一轮早上提交代码下午才出报告定位问题全靠猜。后来我认真做了一次性能梳理把Selenium自动化测试里那些拖后腿的环节一个一个揪出来替换掉、裁剪掉、并行掉整体回归时间从原来的一百多分钟压到了四十分钟上下。这个提升幅度不是什么玄学就是实打实地把等待、选择器、浏览器配置、执行策略这些环节挨个优化了一遍。这篇文章我把其中最有效的10个技巧整理出来每个都附上思路、代码和实测数据覆盖从单条用例到整套回归的不同优化层面希望能帮你把自动化测试的速度也拉起来。1. 先搞清楚你的测试到底慢在哪里1.1 瓶颈分析Selenium慢的根源往往不在浏览器很多人一提到Selenium慢第一反应是换成别的工具或者抱怨WebDriver本身性能不行。但我实际排查下来真正导致速度上不去的绝大部分是脚本本身的问题。我总结下来Selenium自动化慢的根源基本集中在四个层面等待策略不合理大量使用固定等待不管页面到底要多久才加载完一律time.sleep(5)。单个用例可能只浪费两三秒但几百条用例叠加起来这个数字就非常可怕。定位器效率低XPath写得过于复杂比如从根节点一路往下写或者依赖页面结构多层嵌套。浏览器每执行一次定位都要遍历大量节点这种损耗在数据表格和大列表页面上尤其明显。浏览器配置冗余默认配置会加载图片、字体、CSS、JS等各种资源而这些资源对自动化断言来说往往毫无价值。执行方式串行一条用例跑完再跑下一条完全没有利用现代CPU多核的特性。一个8核的机器自动化只用一个核等于浪费了87.5%的算力。当你理解慢是分布在多个层面的就不会再指望通过某一个技巧解决所有问题。这也是为什么我建议先做瓶颈分析而不是直接照搬优化方案因为不同项目的瓶颈权重不一样有的是等待浪费多有的是选择器太重有的纯粹是串行跑完的。1.2 优化前先建立基准数据动手优化前我建议你先做一件事记录当前回归测试的基准数据。具体来说就是三个指标总执行时长整套回归从启动到全部出结果的时间。单用例平均时长拿到所有用例时长的平均值和中位数中位数更重要因为它不受个别极慢用例干扰。慢用例Top 10统计哪些用例耗时最长。拿到这三个数据以后再去决定优化优先级。我的经验是永远先处理Top慢用例因为少数几个极慢的用例往往占了总时长的大头等它们降下来了再做并行和全局配置的优化。我这边当时的情况是全套回归两百来条用例总时长约135分钟单用例中位数30秒左右但Top 10的用例里最慢的能到5分钟以上明显是某些页面加载和等待策略出了问题。优化完等待和选择器以后总时长直接降到了八十多分钟。所以你如果问我第一步做什么我肯定会说先量数据再动刀。2. 技巧1-2等待和定位是单条用例提速的关键2.1 技巧1用显式等待替代固定sleep固定等待是我见过最普遍、也最浪费时间的写法。以前很多教程喜欢教time.sleep(5)页面加载慢就写大一点8秒、10秒都敢写。问题是页面加载快的时候这5到10秒白等了加载慢的时候5秒可能还不够。不管哪种情况都是在浪费时间和制造不稳定。正确的做法是用显式等待也就是让脚本等到元素出现、可点击、可见之后再继续执行。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 替换 time.sleep(8) element WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, submit-btn)) ) element.click()这段代码的逻辑是最多等10秒但元素一旦可点击就立刻执行不用多等一毫秒。相比之下固定sleep的逻辑是无条件等满8秒再继续。我实际项目中做过一个对比一套单页面的数据录入用例原版用了6个time.sleep(3)跑完需要25秒左右改成显式等待以后跑完降到9秒。原因很简单本来每个步骤都要白等3秒现在页面响应快的时候几乎零等待。需要注意一点显式等待的条件要选对比如等待元素可见用visibility_of_element_located等待元素可点击用element_to_be_clickable等待页面某个文案出现用text_to_be_present_in_element。选错条件会导致等待无效还是容易出现误判。另外不要用显式等待替代implicitly_wait它们俩是不同机制的。隐式等待是每次查找元素时轮询显式等待是独立线程轮询混用会让等待时间叠加反而更慢这也是一个容易被忽略的坑。2.2 技巧2优化选择器避开复杂XPath和慢速定位XPath不是不能用但很多人写的XPath效率极低。最常见的问题有两类绝对路径比如/html/body/div[3]/div[2]/form/div[1]/input这种。一旦页面结构有调整直接失效而且浏览器执行这种定位时是一层一层往下遍历性能很差。模糊匹配比如//div[contains(class, wrapper)]//div[contains(class, content)]//span[contains(text(), 确认)]这种写法每执行一次定位都要遍历页面上一堆节点去匹配字符串成本非常高。优先级最高的定位器永远是id然后是name、CSS选择器、合理的XPath。原因很简单浏览器对id有原生的快速查找机制直接哈希定位几乎不消耗时间。CSS选择器也经过了浏览器高度优化执行效率很高。# 低效写法 submit_btn driver.find_element(By.XPATH, /html/body/div[3]/div[2]/form/div[5]/button) # 高效写法 submit_btn driver.find_element(By.CSS_SELECTOR, #submit-btn)如果页面确实没有提供合适的id和class需要依赖XPath定位也要尽量缩短路径深度用相对路径而不是绝对路径。同时结合业务特征利用元素的父级兄弟关系来缩小范围而不是从根一路穿透下来。这个优化对单条用例来说可能只影响几百毫秒但对于包含大量操作的长流程用例来说累积起来效果很明显。我手里的项目里有个长流程用例原本定位了40多次元素复杂度高耗时长把定位器全部换成id和短CSS选择器以后单条用例快了将近一倍。提示如果前端代码是你自己团队的最根本的解法是在开发阶段就给关键交互元素加上稳定的>from selenium.webdriver.chrome.options import Options options Options() prefs { profile.managed_default_content_settings.images: 2, profile.managed_default_content_settings.stylesheets: 2, profile.managed_default_content_settings.fonts: 2 } options.add_experimental_option(prefs, prefs) driver webdriver.Chrome(optionsoptions)profile.managed_default_content_settings里的值2代表禁用1代表允许。禁用图片之后很多电商、资讯类页面的加载时间能明显缩短样式表这一项要谨慎因为某些断言依赖元素的视觉样式比如颜色值校验这时候就不能禁CSS。禁用字体在极少数情况下会影响布局计算但大多数业务断言不关心字体渲染结果所以放心用。我自己实测过一个靠大量图片展示的营销页禁用图片后加载时间从6秒降到3秒以内效果非常直接。有一点要提醒如果你依赖某些需要网络请求的第三方统计脚本或者页面里的数据是通过异步请求加载的禁用资源不会影响这类逻辑因为异步请求走的是XHR或fetch不受图片和样式设置控制。3.2 技巧4无头模式不是银弹但确实能省渲染时间无头模式Headless是很多人优化Selenium第一件想做的事因为它确实省掉了浏览器渲染界面的开销。但无头模式有个问题它和真实浏览器的行为不完全一致尤其是以下场景某些元素的可点击区域判定和可见性判定会有细微差异。部分页面在无头模式下压根不会触发某个JS初始化逻辑。依赖视口尺寸的响应式布局在无头模式下默认视口是800x600容易掩盖真实用户环境的问题。所以我的建议是本地调试阶段用有头模式CI环境跑大批量回归时再用无头模式。无头模式在CI里省了渲染资源整体跑起来也更稳定因为不会受到屏幕分辨率切换、弹窗遮挡等因素干扰。options Options() options.add_argument(--headlessnew) options.add_argument(--window-size1920,1080) options.add_argument(--disable-gpu) driver webdriver.Chrome(optionsoptions)注意Chrome新版的无头模式参数是--headlessnew老版本是--headless。在实际使用中--window-size一定要设置不然页面按默认视口渲染很多元素的位置和你预期的完全不同。--disable-gpu在无头模式下已经不是一个关键参数了但加上也无妨尤其在CI里用的是虚拟环境的时候。无头模式的实际性能提升幅度要看页面本身的渲染开销。对于SPA应用来说能省20%到30%的执行时间对于传统多页面跳转的应用提升会小一些因为主要耗时在网络请求上而不是页面渲染。3.3 技巧5复用浏览器会话减少启动和销毁开销浏览器启动和WebDriver会话建立是比较重的操作每次启动都要初始化进程、加载插件、建立通信通道。如果你有几十条用例每条用例都启动一个全新的浏览器再关掉光启动和销毁的时间就是一大块浪费。减少这部分开销的常规做法是会话复用也就是让多条用例共用同一个浏览器实例。实现方式有两种在测试框架层面把WebDriver的生命周期从方法级提升到类级或套件级。使用Selenium Grid的持久化会话或者通过options.add_experimental_option(debuggerAddress, ...)重用现有的浏览器实例。第一种方式实现简单适合在pytest里通过fixture来控制import pytest from selenium import webdriver pytest.fixture(scopeclass) def driver(): options webdriver.ChromeOptions() options.add_argument(--disable-extensions) driver webdriver.Chrome(optionsoptions) yield driver driver.quit()scopeclass意味着整个测试类共用同一个浏览器而不是每个方法都用一个新的。这个改动带来两个好处省掉了重复启动的耗时同时因为浏览器Cookie和Session一直在登录状态也能复用对于需要反复登录的场景特别划算。复用会话的代价是用例之间的隔离性变差如果上一条用例把浏览器状态搞乱了下一条会受影响。所以这个技巧更适合业务路径连续的用例组不适合完全独立的单元测试式用例。你需要在速度和隔离性之间找到一个自己的平衡点。4. 技巧6-8从单条提速到整套回归提速4.1 技巧6同一机器上多线程并行把CPU利用率拉满单条用例优化得再好如果整套回归还是串行执行速度天花板就在那里。并行执行是提速最猛的手段提升幅度往往以倍数计算。前提是你的机器CPU核数够用。一台8核16线程的机器理论上可以跑8到12个并行任务。Selenium脚本本身大部分时间在等待网络请求和页面渲染CPU占用并不高所以并行数甚至可以超过CPU线程数。在pytest里最简单的方式是用pytest-xdist插件pytest -n 4 --dist loadscope-n 4表示开4个worker进程--dist loadscope表示按测试类或测试模块来分配任务保证同一个类下的用例不会分散到不同worker去避免共享浏览器状态的冲突。有人担心并行之后测试结果不稳定这是个真实存在的问题但根源通常不是并行本身而是用例之间有隐式依赖比如某条用例假设前面某条已经创建了某个数据。这种用例在串行时可能碰巧没事一并行就暴露出来了。我以前也经历过类似情况某个用例平时跑得好好的并行之后偶尔失败查了半天才发现它竟然依赖另一条用例创建的临时账号。遇到这种问题修正的方式是把依赖的数据准备放到用例自身中来也就是做数据自包含而不是禁用并行。并行执行还有个隐藏收益同一个批次的用例会在几乎同一时间打到被测服务上从服务端视角看这更像真实用户并发暴露并发问题的概率也更高。4.2 技巧7跨机器分布式执行突破单机瓶颈单机并行有上限CPU核数、内存、带宽都会成为瓶颈。当测试规模进一步扩大或者需要覆盖不同的浏览器和操作系统组合时就该上分布式方案了。Selenium Grid是经典的方案架构上就是一台Hub或Router加多个NodeNode可以是本机的也可以是远程机器的。任务来了以后Hub会把请求分配给合适的Node去执行。以Selenium Grid 4为例启动方式很简单# 启动Router/Hub java -jar selenium-server-4.0.0.jar hub # 在另一台机器上启动Node java -jar selenium-server-4.0.0.jar node --detect-drivers测试代码里把远程执行的地址指到Hub就行from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() driver webdriver.Remote( command_executorhttp://grid-server:4444, optionsoptions )Grid的价值在于你可以在多台机器上注册多个Node然后并行量就不再受单机资源限制。它能解决的不光是速度问题还有浏览器矩阵的覆盖问题比如有的用例要跑Chrome有的要跑Firefox或Safari在一个Node上反复切浏览器很费时间但多个Node各跑各的总耗时就下来了。不过Grid本身不解决用例的依赖问题它只是把任务分发到不同节点。如果用例之间有状态依赖即便上了Grid也一样会出现随机失败。所以我的建议是先在单机并行阶段把用例的独立性理顺再考虑上分布式。4.3 技巧8合理拆分用例粒度把大用例打碎再重组并行和分布式的优化有一个前提用例粒度不能太大。一条用例如果从登录开始到创建订单再到验证订单列表最后做数据清理全程50步这中间不管是等待、定位还是重试任何一个环节出问题整条用例的时间都会被拉得很长。把大用例打碎成小用例听起来会增加用例总数但总时间反而可能下降。原因有两个小用例失败后重试的成本低大用例重试一次可能就要重跑很多无关步骤。用例变短以后并行调度的灵活性更高调度器可以把更多小用例分散到不同worker上减少某个worker空转的时间。具体怎么拆分我常用的原则是按业务边界拆。登录、创建数据、操作业务、验证结果这几个阶段如果相互独立就拆成不同的用例如果前后依赖就保持串联。不能为了拆而拆否则用例之间的数据准备会让整个框架复杂很多。一个具体的场景原来有30条长流程用例每条大约4分钟串行跑完120分钟。我把公共操作抽成基础步骤拆成80条小用例每条平均1分钟左右再配合8路并行跑完不到20分钟。用例是变多了但总时间降了一个数量级。注意拆用例的时候不要把关键业务链路拆断。比如支付场景从创建订单到支付成功再到订单状态变更这整个过程必须是一条完整的用例来验证拆散之后反而丢失了链路联动的覆盖价值。5. 技巧9-10脚本设计和数据层的隐藏耗时5.1 技巧9页面对象模型复用消灭重复加载和初始化开销页面对象模型Page Object Model是Selenium自动化里的经典设计模式核心思想是把页面元素定位和操作方法封装成独立的类。这个模式最大的优点是维护性好但它对性能的影响经常被忽略。很多实现里每个测试方法都会先实例化一个页面对象然后页面对象在构造方法里做一堆初始化工作比如查找页面元素、加载配置数据、初始化WebDriverWait甚至有的还会打开页面。这样每条用例跑起来都有额外的初始化和页面加载时间。正确做法是把页面对象设计成轻量级的构造方法里只接收WebDriver实例不做任何元素查找元素定位放在方法里按需执行常用的元素集合用懒加载缓存。class LoginPage: def __init__(self, driver): self.driver driver # 不做任何find_element操作保持轻量 property def username_input(self): return self.driver.find_element(By.ID, username)对比一下如果构造方法里立刻去查找所有元素页面还没加载完就会报错或者你得额外加等待而按需查找配合显式等待既准确又不会浪费时间。页面对象的复用还体现在登录上。一个项目中往往很多用例都需要先登录如果每条用例都从头登录一遍总授权时间和页面跳转时间都是一笔不小的开销。可以在浏览器会话复用的基础上把登录做成一次性的会话级操作后续用例直接从已登录状态开始跑。我优化过的一个项目里就是因为登录操作被重复执行了太多次光登录耗时占总时长的比例高达三成。后来把登录提升到会话级再结合会话复用整套回归的时间直接砍掉了四分之一。5.2 技巧10截图和日志策略别把时间浪费在没必要的记录上最后一个优化点很多人注意不到但它积少成多的效应非常显著截图和日志。很多团队会配置在每一步操作后截图或者失败自动截图。截图本身倒不慢但保存截图、上传到测试管理平台、生成报告的过程很费时间尤其是失败截图还涉及捕获页面源码这个操作在大型页面上可能要消耗一两秒。一次失败一两秒还好如果一条用例中间步骤多每个步骤都截图累积起来就非常可观。我的建议是三步走只在关键节点截图比如用例结束、断言失败、支付成功/失败这类有业务意义的节点而不是每个click和input都截。按需开启页面源码捕获默认不捕获源码只有断言失败时才捕获用于排查问题。日志分级把会话级日志和步骤级日志分开详情日志只在调试模式输出正常运行时只输出步骤摘要和最终结果。日志这块还有个性能陷阱直接把大量内容print到控制台。在并行执行的时候多个进程同时输出大段内容控制台和日志文件的IO会成为瓶颈严重的甚至导致进程间互相等待。这个问题在CI环境里尤其坑因为CI日志搜集本身也会占用时间。解决方式是用logging模块控制输出级别或者把日志写到不同的文件里避免集中写入。关于截图如果确实需要每一步留证我建议用美工刀策略异步截图。也就是说操作继续跑截图操作放到后台线程里去执行避免截图阻塞当前用例的执行流程。不过异步截图会增加框架复杂度除非有硬性要求否则我一般直接建议减少截图点。6. 常见问题排查速查表优化Selenium性能的路上有些问题非常典型我在不同项目里反复遇到过这里整理成一张速查表方便你对照排查。症状常见原因解决思路某条用例单独跑很快整套跑很慢等待策略多为固定sleep页面响应快时仍在空等全面替换为显式等待并行后用例随机失败用例间存在隐式数据依赖串行时被掩盖让用例数据自包含或恢复到串行定位问题无头模式下元素不可见视口尺寸未设置页面按默认尺寸渲染加上--window-size1920,1080CI里日志收集非常耗时大量细节日志被print到控制台IO成瓶颈日志分级按级别控制输出页面加载时间很长但业务操作正常图片、字体和样式资源加载拖慢整体渲染用ChromeOptions禁用非必要资源多条用例都在重复登录登录操作未提升为会话级在框架层把登录设为一条前置用例或共享会话使用XPath定位非常慢定位器包含深层路径或大量模糊匹配优先使用id其次用简洁CSS选择器这张表是经验层面的汇总不是每个项目都全中。排查的时候还是建议从基准数据出发先确认慢在哪里再对照表里的方向去验证。7. 实际优化效果复盘再来说说我开头提到的项目一套面向电商场景的Web回归测试两百多条用例最开始跑了135分钟。用的是pytest加Selenium页面涉及商品列表、购物车、下单、支付和订单管理数据量中等每个页面做两到三次断言。第一轮优化只做等待策略替换把所有time.sleep改成显式等待总时长降到约85分钟效果最明显的是慢用例原来5分钟的用例直接压到2分钟左右。第二轮优化选择器把复杂的XPath改成id和CSS选择器总时长降到约70分钟。这轮提升幅度不算特别大但稳定性好了很多定位报错明显减少。第三轮就是并行和会话复用用pytest-xdist开8路并行配合登录和浏览器会话复用总时长最终压到约40分钟。相对最初的135分钟整体提升约70%。所以说60%这个数字不是拍脑袋只要把等待、选择器、浏览器配置和并行这几块做到位达到这个量级并不夸张。当然不是所有项目都能跑到这个幅度。如果你的项目网络环境本来就很稳定、页面响应也快、用例已经做得足够原子化可压缩的空间自然小。但绝大部分团队的项目不是这种状态总会存在等待浪费、重复登录、串行执行这三座大山随便搬掉一座都能看到明显变化。8. 最后分享一点个人心得优化Selenium性能这件事我最大的体会是先量化再优化。不要凭感觉猜瓶颈在哪用数据说话。记录基线、单条用例耗时、慢用例分布这些数据会告诉你优先级在哪里。另外不要把优化看成一次性动作。业务在变、页面在变、用例在变上一次的优化方案可能过几个月就不适用了。我自己的习惯是每轮回归跑完后自动记录每个用例的耗时到一份报告里按周对比趋势一旦发现某些用例的耗时在缓慢上涨就提前排查避免问题积累成大坑。最后再提醒一句性能优化永远不要牺牲稳定性和可维护性。一个跑得很快但三天两头报错的测试套件比一个慢但稳定的套件更让人崩溃。优化每一步都做完回归验证确定没有引入新的不稳定因素再进入下一步。自动化测试最终要服务的是研发效率如果为了快而牺牲了可信度那就本末倒置了。
阅读完成 · 觉得有帮助?