资讯动态

Selenium自动化测试实战:从WebDriver原理到落地避坑指南

发布时间:2026/10/20 8:54:09 来源:尧图企业网站定制
做测试这些年Selenium是我接触得最早、用得不间断、也踩坑最深的一个工具。不管你是刚转行想做软件测试的新人还是已经写了两三年脚本的测试开发只要聊到自动化测试Selenium 几乎是绕不开的那个名字。它是开源免费的支持多浏览器、多语言能帮你把重复性的回归测试交给机器去跑让测试人员从“手动点一遍又一遍”的体力活里解放出来把时间花在更有价值的地方。这篇文章不会去复述官方文档而是按我实际做项目的经验把“Selenium自动化测试”这件事从头到尾捋一遍从为什么选它、核心原理是什么到环境搭建、常用操作、问题排查再到落地时容易忽视的坑。如果你正准备在公司里搭建一套UI自动化测试体系或者单纯想搞懂这东西怎么玩这篇文章应该能让你少走不少弯路。1. 为什么偏偏是Selenium1.1 自动化测试到底解决了什么问题先说痛点。一个稍微成型的产品核心功能假设有三四十个用例每个用例平均点十几次页面一轮回归少说也要两三个小时。遇到发版频繁的时候一周回归两三次光这一件事就能占据测试人员大半精力。人一直重复做同样的事情除了效率低还有两个很要命的隐患一是容易疲劳漏点真正出问题的地方反而没注意到二是新人上手慢业务流程藏在老员工脑子里测好测坏全凭个人手感。自动化测试的核心价值不是“代替手工”而是把重复性的回归验证变成机器可以随时执行的任务。晚上提交了代码CI自动跑一遍核心流程第二天早上打开记录看结果省下来的几个小时就能拿去做探索性测试、性能测试或者风险评估。Selenium正是在这个场景下最常见的落地工具它模拟用户在浏览器里的真实操作——点击、输入、翻页、提交——再对页面结果做校验告诉你功能到底有没有挂。1.2 Selenium凭什么成为行业默认选择有人可能会问现在工具那么多为什么多数团队一提UI自动化就先想到Selenium我用过几种不同的方案说说我的理解。首先是开源免费没有授权成本企业用起来没有心理负担。其次是支持多浏览器Chrome、Firefox、Edge都能跑这在兼容性测试里特别重要同一套脚本换一个浏览器驱动就能执行。第三是语言绑定多Java、Python、C#、JavaScript都有官方库团队会什么语言就能用什么写不用为了测试单独学一套玩具语言。再加上社区活跃遇到问题搜索一下基本都有答案不像某些商业工具出问题只能提工单等对方回复。相比之下商业录制回放工具上手快但脚本的可维护性和二次开发能力就差了不少。录制出来的脚本一换页面布局就废又很难嵌入到自己的CI流程里。我个人观点是如果团队有基础代码能力直接上Selenium如果测试团队完全没写过代码也别指望工具能救你赶紧招人或培训才是正道。2. 吃透Selenium的核心机制再动手2.1 WebDriver到底在做什么很多人写Selenium脚本写了一年还是没搞明白它到底是怎么控制浏览器的。我尽量用大白话讲清楚。Selenium发展到WebDriver阶段整体架构可以理解成三个角色你的测试脚本客户端、浏览器驱动翻译官、浏览器本身执行者。测试脚本通过Selenium库发送标准化的操作指令比如“点击这个按钮”“往输入框填文字”这些指令不是直接发给浏览器的而是发给浏览器驱动。浏览器驱动接到指令后把它翻译成浏览器能理解的原生操作并驱动真实浏览器页面执行再把执行结果返回给脚本。打个比方你想让一个只说英语的人帮你做事你自己不会英语于是你请了一个翻译。你对翻译说中文指令翻译转成英语告诉对方对方照做后再通过翻译告诉你结果。在这个过程里浏览器就是那个“只懂英语的办事员”浏览器驱动就是翻译官。理解这个机制对你排查问题特别关键。很多人遇到“脚本明明写了点击页面就是没反应”这类问题第一反应是去改定位表达式但真正的原因往往是浏览器驱动版本和浏览器版本对不上指令根本没送到浏览器那里。不懂原理你会一直朝错误的方向使劲。2.2 元素定位的八种方式与选型逻辑自动化操作页面的前提是先找到页面上那个元素。Selenium提供了八种定位方式id、name、class name、tag name、link text、partial link text、xpath、css selector。我用下来的经验是能用到id和name就用id和name。id在页面里通常是唯一的定位速度快、写法简单、代码可读性也好。排第二的是css selector它定位效率高语法简洁遇到没有id和name的元素时优先考虑。xpath是能力最强的页面结构复杂的时候基本都能兜底但它有个问题页面结构调整时xpath的维护成本很高特别是那种从某个祖先节点一路写下来的绝对路径看着就头大。举几个实际例子。比如登录按钮长这样button idlogin-btn namelogin登录/button直接用driver.find_element(By.ID, login-btn)即可。如果没有id可以试soup driver.find_element(By.CSS_SELECTOR, button[namelogin])。如果连name也没有再用xpath比如//button[contains(text(),登录)]。我特别想强调一点不要一上来就无脑复制浏览器“开发者工具”里生成的xpath。那种路径经常长成这样/html/body/div[2]/div[3]/div/div[1]/form/div[2]/input页面上随便加一层div就全断了。我一般会用相对xpath配合属性或文本定位尽量控制在几层之内比如//input[placeholder请输入手机号]稳定性和可读性都更好。2.3 等待机制脚本“偶发失败”的罪魁祸首新手写Selenium脚本最常见的坑就是把自动化当成“瞬间操作”。页面元素还没加载完就去找它脚本自然报找不到元素。解决方案就是等待但等待也有讲究。Selenium有三种等待方式强制等待time.sleep()、隐式等待implicitly_wait、显式等待WebDriverWait。我现在的项目里几乎不用强制等待偶尔调试阶段临时看一眼用一下。隐式等待相当于给查找元素设置一个全局兜底时限比如10秒元素在那段时间内出现就立即继续超时才报错。它写起来简单但问题在于只能等元素出现不能等元素“可点击”“可见”这些更细的状态。实际项目中我更推荐显式等待它可以精确等待某个条件成立。比如等一个按钮变成可点击状态再执行点击操作from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait wait WebDriverWait(driver, 10) wait.until(EC.element_to_be_clickable((By.ID, submit-btn)))为什么推荐显式等待因为很多交互按钮是在接口返回数据之后才由异步逻辑激活的。元素在DOM里存在但可能是灰色的不可点状态直接点击会提示“元素不可交互”。显式等待能把这种异步渲染的时序问题消化掉脚本稳定性一下子就上来了。我在实际项目里还踩过一个细节有些动画效果的展开菜单元素明明显示出来了但位置还在移动点击会点偏。这种场景我会加一层wait.until(EC.visibility_of_element_located())再加短暂停顿确保元素完全稳定再操作。3. 从零搭建Selenium自动化测试环境3.1 环境准备三件套Python、Selenium库、浏览器驱动我以Python为例这是目前做测试开发最主流的语言。首先用pip install selenium安装Selenium库这个没什么难度。需要重点说明的是浏览器驱动的下载与版本匹配。以Chrome为例你要到某个镜像站下载与你本机Chrome主版本号一致的chromedriver。比如本机Chrome是114版本最好下载114开头的chromedriver主版本不一致基本跑不起来启动浏览器的时候直接报session not created或者unknown error。版本对不上这件事我遇到不下十次用户一问都是“我明明装了最新的chromedriver啊”结果一看Chrome版本旧了好几代。Firefox对应的是geckodriverEdge对应的是edgedriver道理一模一样。下载完之后我习惯把驱动放到一个固定的目录下用代码指定from selenium import webdriver from selenium.webdriver.chrome.service import Service service Service(rD:\tools\chromedriver\chromedriver.exe) driver webdriver.Chrome(serviceservice)如果你不想手动管理驱动版本可以用第三方库webdriver-manager它可以自动检测本地浏览器版本并下载匹配的驱动。但自动化测试环境讲究可控团队共用一套CI环境时手动固定版本反而更稳妥。我的建议是本地开发随便用CI环境里锁定版本。3.2 第一个能跑通的Selenium脚本环境准备好之后写一个最简单的脚本打开某网站、输入关键词、点击搜索、验证结果。完整代码如下from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver webdriver.Chrome() try: driver.get(https://example-site.com) input_box WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, search-input)) ) input_box.send_keys(自动化测试) input_box.send_keys(Keys.ENTER) result_area WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CLASS_NAME, result-list)) ) results result_area.find_elements(By.CSS_SELECTOR, .result-item) assert len(results) 0, 搜索结果为空 print(测试通过找到了 {} 条结果.format(len(results))) finally: driver.quit()别小看这段最简单的脚本它已经包含了一个合格自动化用例的基本骨架初始化浏览器、加载页面、等待关键元素出现、执行操作、对结果做断言、最后释放浏览器进程。driver.quit()这一步特别容易被新手忽略只写driver.close()甚至干脆不关。程序跑完浏览器进程还挂在那下次执行就报端口被占用。我在实际项目中彻底关干净推荐quit()它会完整结束所有相关进程。3.3 从“脚本”到“测试用例”引入pytest上面那段代码只是一段能运行的脚本还不是真正的测试用例。我会把它放到pytest框架里用断言、fixture来管理生命周期。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 pytest.fixture def driver(): d webdriver.Chrome() yield d d.quit() def test_search_result(driver): driver.get(https://example-site.com) input_box WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, search-input)) ) input_box.send_keys(自动化测试) input_box.submit() result_area WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CLASS_NAME, result-list)) ) results result_area.find_elements(By.CSS_SELECTOR, .result-item) assert len(results) 0, 搜索结果为空fixture在这里的作用是每个测试用例执行前启动浏览器、执行完后关闭浏览器。这样多个用例之间环境隔离互不干扰。把脚本转化成pytest用例是我认为从“能跑”到“能维护”最关键的一步。后面还可以给fixture加scopesession之类的参数让多个用例复用同一个浏览器会话减少启动耗时那是进阶话题后面再细说。4. 高频场景的实操手册4.1 最常见的页面交互操作自动化测试里最常用的操作翻来覆去就是那么几类输入文字、点击按钮、选下拉框、处理弹窗、切换窗口和iframe。每一个都有需要注意的细节。输入文字用send_keys()这个大多数人都会。但是我建议不要在用户名字段用send_keys()之后直接提交按钮因为有些页面会做实时校验你输入太快、触发事件的时机不对会漏掉校验逻辑。稳妥的做法是输入完之后做一次断言确认输入框的值已经正确回显再点提交。下拉框推荐用Selenium的Select类from selenium.webdriver.support.ui import Select select Select(driver.find_element(By.ID, city-select)) select.select_by_visible_text(上海)它支持通过索引、value属性、可见文本三种方式选值。实际中我偏爱用select_by_visible_text因为可读性最好别人看代码时一眼就明白选的是什么。处理iframe是个高频坑。如果页面里操作的目标元素在iframe内直接find_element是找不到的必须先切换进去frame driver.find_element(By.CSS_SELECTOR, iframe[srcupload]) driver.switch_to.frame(frame) # 在iframe内操作 driver.find_element(By.ID, file-input).send_keys(file_path) # 操作完切回默认内容 driver.switch_to.default_content()别忘了一个细节操作完iframe里的内容要继续操作主页面的元素必须切回默认内容不然Selenium还是会傻乎乎地跑到iframe里找下一个元素然后报找不到。弹窗处理也简单switch_to.alert能拿到alert对象用accept()点确定dismiss()点取消。不过现在很多网站的弹窗是自定义的H5弹层不是系统弹窗遇到那种情况还是要走元素定位去点击右上角的关闭按钮。4.2 文件上传与下载文件上传在自动化测试里经常遇见比如传头像、传附件。如果是input typefile这种原生控件是最省事的直接把本地文件路径传给send_keys()就行upload_input driver.find_element(By.ID, upload-file) upload_input.send_keys(rC:\test_files\test_data.xlsx)有些上传组件隐藏了input或者做了更复杂的交互就不能这么直接。我遇到过一个项目用的是自定义拖拽上传光靠send_keys没法触发。稳妥的办法是把input元素的display属性改出来或者直接用execute_script模拟拖拽事件。这里提供一个曲线救国的方式用JavaScript强制把元素设为可见再配合send_keys操作。文件下载的话Chrome默认会自动下载到下载目录Selenium本身不干预。如果要固定下载路径或者禁止弹窗用ChromeOptions配置prefsoptions webdriver.ChromeOptions() prefs { download.default_directory: rC:\test_files\downloads, download.prompt_for_download: False, } options.add_experimental_option(prefs, prefs) driver webdriver.Chrome(optionsoptions)下载完文件后还需要验证文件是否存在、大小是否合理。这里有个小技巧不要下载完立刻判断文件存在因为文件写入需要时间。做一个简单的轮询等待两秒检查一次最多等二十秒文件出现且大小不再变化才算下载完成。4.3 失败截图与日志埋点UI自动化用例跑挂了最让人崩溃的是只看报错信息完全不知道页面当时发生了什么。一张现场截图能省掉大量排查时间。我习惯在pytest的fixture里做失败截图具体思路是用例执行结束后判断如果测试失败就在失败时机截屏保存。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.strftime(%Y%m%d_%H%M%S) driver.save_screenshot(fscreenshots/{item.name}_{timestamp}.png)截图保存下来的命名格式一定要带上用例名和时间戳不然几十张error.png根本分不清是哪条用例。另外我还会在关键步骤前加日志输出用logging.info记下每个业务流程的路径比如“已打开登录页”“已输入账号”“点击登录按钮”。用例跑挂了翻日志就能大概定位到是哪个环节出了问题比对着报错瞎猜高效得多。还有一个进阶细节截图不要只截当前视口遇到长页面最好先把页面滚到底部再滚回顶部触发首屏懒加载确保截图里能看到完整页面内容。或者直接用driver.set_window_size把窗口调大一点再截图有些元素在窗口外截不到会误导定位分析。4.4 数据驱动把数据从代码里拆出来一个登录用例点击发送按钮的行为是固定的变化的是不同用户、不同密码、不同预期结果。把这些数据从代码里硬拆出来这就是数据驱动测试。最简单的做法就是pytest的参数化pytest.mark.parametrize(username,password,expected, [ (user_a, pass123, 登录成功), (user_b, wrong_pass, 密码错误), ]) def test_login(driver, username, password, expected): # 执行登录逻辑 # 断言结果包含 expected跑到第三个数据的时候挂了pytest会清楚告诉你第三个数据组失败不会像在for循环里一样整个用例红了还不容易定位。数据多了之后可以从excel、json或者yaml文件里读写一个数据读取函数把测试数据维护从代码层面完全剥离出去。这样测试用例文件的职责就只剩下“测什么行为”而不是“用什么数据测”。5. 踩过的坑常见问题与排查清单5.1 元素定位失败但页面明明有这个元素大概是我见过最多的一类问题而且新手碰到最容易钻牛角尖明明用开发者工具能看到这个元素脚本它就是找不到。原因一般逃不出这四种第一元素在iframe里。前面讲过需要先switch_to.frame切进去。第二元素在新打开的窗口里。点击按钮后打开了新标签页但Selenium仍然停留在原页面要用driver.window_handles切换窗口句柄。第三元素是动态加载的页面滚动到可见区域才渲染出来。解决方案是driver.execute_script(arguments[0].scrollIntoView();, element)滚动过去再定位。第四元素存在但被遮挡或不可见。比如弹层盖住了按钮或者按钮本身是隐藏状态此时要先处理遮挡物或者改用JavaScript直接点击。我的排查顺序是先打印driver.page_source看看DOM里到底有没有这个元素再判断它是不是在某个frame里再检查窗口句柄最后看不可见的原因。别一上来就怀疑自己xpath写错了那会浪费大量时间。5.2 浏览器驱动版本不一致导致的各种报错Selenium的版本兼容性问题非常常见。Chrome浏览器自动更新了还没落到chromedriver启动浏览器时就会报各种奇怪的错误什么unknown error: cannot find Chrome binary、session not created: This version of ChromeDriver only supports Chrome version 114。解决办法就是在环境里固定浏览器版本和驱动版本并且在CI配置里写清楚。我管理的自动化环境是禁止Chrome自动更新的Windows更新策略、云主机的自动更新范围都要检查一遍。团队里有人本地跑不出来先问版本对不对大概率是版本错位。关于Selenium版本我自己从4.0开始就不推荐再做古老的find_element_by_id这种写法了直接用具名参数By.ID。而且4.x版本内置了相对定位器查找“某个按钮旁边的输入框”会方便很多值得去了解一下。5.3 测试用例之间的数据隔离问题自动化测试最烦的一件事昨天还全绿今天早上跑出来三条红的一查原因是前一条用例把测试环境的数据状态改掉了。比如A用例给某个商品下单B用例又去验证同样这个商品是否可售两者没有隔离好互相影响。解决方案有几个层面。第一让每个用例自己准备测试数据测试前创建数据测试后清理不要依赖别人造好的数据。第二使用独立的测试账号和测试数据池避免跟手工测试人员抢资源。第三用例之间尽量不要共享“唯一的页面状态”比如你负责的用例改变了用户登录状态那么后面所有依赖登录的用例都可能会挂。我也遇到过用scopesession复用浏览器会话导致用例间状态污染的情况。比如一条用例退出了登录同一会话的下一个用例想操作登录后的页面自然就翻车了。后来我改成按模块复用会话模块边界切清楚才稳定下来。5.4 脚本跑不动稳定性优化三板斧UI自动化最被诟病的点就是“不稳定”同样的脚本刚才还过现在挂换台机器挂了网络慢一点也挂了。我从崩溃里总结的三板斧是等待、重试、独立环境。等待的事前面说过显式等待是必须的。重试的话有两种思路一种是在业务操作层面做重试比如点击一个按钮后校验下一个页面是否出现没出现就重新点一次另一种是pytest插件层面做失败重试比如pytest-rerunfailures可以指定重跑次数。我的原则是重试用来兜底“网络抖动、页面样式偶尔延迟刷新”这种偶发因素绝对不能用来掩盖业务逻辑真正的bug。如果一个用例反复重跑才能过它本身的质量就值得怀疑。独立环境就更重要了。UI自动化最好跑在专门部署的测试环境上不跟手工测试的环境混用。数据干净、版本固定、前端样式不会被其他测试操作影响脚本稳定性会有质的提升。我在公司里见过最夸张的情况是自动化跑在正式环境旁边的预发布环境里结果预发布环境每天被其他人手动测试点击搞得数据一团糟自动化脚本天天红后来切到独立环境绿色率一下子回到了95%以上。6. 给准备上自动化的人几句大实话6.1 什么样的项目适合做自动化不是所有项目都适合UI自动化。我判断一个项目值不值得做主要看三个条件第一核心流程稳定页面结构不是三天两头变第二回归频率高经常要反复验证同一套功能第三业务价值高核心付款、注册、搜索这些流程一旦出错影响面很大。满足这些条件的项目上Selenium自动化是划算的能快速反馈风险解放人力。另外从技术债务的角度考虑一个项目自动化测试基础再差也比没有强。哪怕先只覆盖登录、注册、找回密码三个核心流程也能保证产品最根本的功能不崩。我见过太多团队做自动化总想一口吃成胖子上来就要覆盖所有功能结果脚本写了一堆维护跟不上最后全废掉。小步快跑先把骨架搭起来再慢慢扩充是更现实的路线。6.2 什么样的项目先别碰自动化后台管理类页面如果改版频繁比如每两周调一次菜单结构、按钮位置那就先别急吼吼上自动化。你花的维护时间可能比手动点点还要多ROI是负的。还有一种是一次性运营活动页上线跑几天就下线脚本用完即弃价值太低纯属浪费工时。我接过的某公司数据分析后台界面调整非常频繁自动化用例跑一次鲜绿一次红最后测算了一下时间成本比手工测试高了不少。后来我们决定核心报表导出场景保留自动化其他动态页面全部撤掉等界面稳定了再重新补。这个取舍团队里当时有不少人反对但三个月后大家发现稳定的用例数量翻了一倍因为团队不用天天救火修定位了。6.3 脚本维护比写脚本难十倍如果只写脚本不考虑维护脚本基本都是死路一条。我见过最典型的反例是测试代码里到处是硬编码的xpath、页面结构一变就要在几十个地方改。根本原因是缺少分层设计。解决手段是Page Object模式PO模式。PO模式的核心思想是把每个页面抽象成一个类页面上的元素和操作封装成这个类的方法测试用例只关心业务逻辑不关心页面上用什么按钮、用什么选择器。举个例子把“登录页”封装成LoginPage类里面包含输入用户名、输入密码、点击登录三个方法测试用例里直接调用这三个方法来完成登录页面再改只需要改LoginPage这一个文件。class LoginPage: def __init__(self, driver): self.driver driver def enter_username(self, username): self.driver.find_element(By.ID, username).send_keys(username) def enter_password(self, password): self.driver.find_element(By.ID, password).send_keys(password) def click_login(self): self.driver.find_element(By.ID, login-button).click()我强烈建议任何一个团队在自动化测试脚本超过一千行之后引入PO模式。它能显著降低后续的维护成本。我目前维护的这套用例两千多个页面对象页面改版时基本只需要动对应的页面类纯测试用例代码很少去翻。另一个维护重点是代码规范。统一的命名、统一的定位方式、统一的等待策略比什么都重要。我会在项目里写一份简单的脚本规范包括变量命名用snake_case、定位优先级id优先、xpath最后、禁止在用例层使用time.sleep、每条用例必须有断言等。这份规范不加约束病毒式扩张的测试代码很快会变成无人敢动的泥潭。最后再说一个个人体会自动化测试是人力的放大器不是人力的替代品。它能把重复劳动自动化但设计用例、分析失败、维护脚本这些工作仍然需要专业的人来做。把它当工具用不神话也不会贬低才是正确姿势。先跑通一个用例再搭建起整个体系这条路我自己走过确实难但走通了你会发现自己对产品的理解和对测试的系统性思考都比以前上了一个台阶。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑