资讯动态

Selenium Web自动化测试实战:环境搭建到稳定用例设计

发布时间:2026/10/15 5:37:12 来源:尧图企业网站定制
1. 为什么我建议用Selenium做Web自动化测试做Web测试这件事如果你还在靠纯手工点页面、截图、填表格那大概率已经被反复回归的测试任务折磨过了。每次发版前要过一遍核心流程点几十个按钮核对十几处文案稍微漏掉一个检查点线上就可能出事。我踩过这个坑所以后来坚决把Selenium这套东西捡了起来。Selenium是目前生态最成熟的开源Web自动化方案没有之一。它支持Python、Java、C#、Ruby等多种语言能驱动Chrome、Firefox、Edge、Safari这些主流浏览器模拟真实用户的操作路径——点按钮、填输入框、下拉选择、拖拽、切换标签页、处理弹窗几乎覆盖人能在浏览器里做的所有事。配合pytest这类测试框架一套脚本可以回归几百条测试用例跑完自动出报告省下的时间和人力非常可观。可能有人会问现在不是有Playwright和Cypress这些后起之秀吗为什么还要用Selenium我的看法是Selenium的WebDriver协议已经成了事实上的行业标准很多云测试平台、内部测试基础设施都围绕它搭建而且它的学习资料最多团队招人也好上手兼容性问题的解法在网上基本都能搜到。如果你想做的是Web UI自动化测试而不是想追新框架选Selenium是稳妥的选择。这篇内容适合的人大致有三类一是刚接触自动化测试、想选择第一个框架入门的测试工程师二是开发偶尔需要自测前端功能、又不想靠手工反复点的工程师三是已经在用Selenium但经常被元素定位、等待时机、用例稳定性这些问题折磨想看一些排坑经验的人。我的分享会以Python和Selenium 4为主线从环境搭建一直讲到完整的测试项目结构中间穿插大量实际踩坑记录。2. 环境搭建看似简单坑全藏在版本匹配里2.1 基础工具箱的准备先把底子打好。我建议直接用Python 3.9以上的版本然后通过pip安装Selenium库pip install selenium只装这个库还不够你还需要一个浏览器驱动Selenium本身不能直接操作浏览器它是通过WebDriver这个中间层去指挥浏览器的。Chrome对应的是ChromeDriverFirefox对应的是geckodriverEdge对应的是msedgedriver。这里有个最常见的坑驱动版本必须和浏览器版本匹配。以前的做法是去驱动下载页面手动下载对应版本还要自己解压、配环境变量换一台机器就要重新折腾一遍。有段时间我被这个事整得特别烦后来发现用WebDriver Manager可以彻底解决。pip install webdriver-managerPython端的使用方式from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice)这一行ChromeDriverManager().install()会自动检测你本机Chrome的版本号去对应位置下载匹配的driver并缓存起来。以后浏览器升级了它也会自动适配不用再手动管。我给团队的自动化框架里就统一做了这个封装新同事拉下代码装上依赖就能跑基本零配置成本。2.2 为什么不建议手动管理驱动手动管理驱动看起来简单但实际维护成本特别高。举个例子某次Chrome自动升级到新版本旧的driver直接罢工所有用例报错排查了半天才发现是版本不匹配。这种问题在多人协作的团队里尤其明显——你机器上是106版本同事机器上是104版本driver又不同脚本在A机器跑得好好的到B机器就挂。用WebDriver Manager这类自动化工具之后这个问题基本绝迹。同样的思路也可以用在Firefox和Edge上包都提供了对应的Manager类。如果公司网络环境受限你也可以把driver文件放在一个共享位置通过Service指定路径加载但日常开发还是建议自动管理。2.3 浏览器配置选项headless、无痕和性能优化装好基础环境后第二个值得花时间研究的是浏览器的启动选项。ChromeOptions是万能钥匙很多测试场景都靠它来配置from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headlessnew) # 无头模式不显示浏览器窗口 options.add_argument(--no-sandbox) # 容器环境经常需要 options.add_argument(--disable-gpu) options.add_argument(--window-size1920,1080) # 避免某些元素因视口太小而不可见 options.add_argument(--disable-notifications) # 禁用通知弹窗 options.add_experimental_option(excludeSwitches, [enable-automation]) driver webdriver.Chrome(optionsoptions)headless模式的意思是浏览器在后台运行不弹窗口特别适合在CI/CD服务器上跑测试。以前的老headless模式加--headless不带new对某些页面渲染有问题Selenium 4配合新版Chrome用--headlessnew效果好很多基本和真实浏览器行为一致了。另外如果你跑的是需要登录态的测试可以手动登录一次把浏览器的user data目录保存下来之后启动时挂载这个目录省得每条用例都走登录流程。这个技巧对ToB系统特别实用因为很多内部系统有验证码或短信验证脚本很难绕过去。注意用user data目录的方式虽然省事但要注意测试用例之间的状态隔离问题。A用例登录后账号状态变了可能影响B用例。建议只对没有状态冲突的用例用这个方案有状态的场景还是走独立账号或独立的用户数据目录。3. 核心机制拆解定位、等待、交互搞懂这三件事就入门了3.1 元素定位八种策略和一条实战铁律Selenium操作页面的前提是能找到页面上的元素。定位策略总共有八种我把它们分成两类来看第一类是属性直查型比较简单快速IDdriver.find_element(By.ID, username)这是最优选择因为HTML规范里ID唯一NameBy.NAME登录表单里很常见Class NameBy.CLASS_NAME注意一个元素的class属性可能有多个值要匹配完整的或用CSS选择器处理Tag NameBy.TAG_NAME一般很少直接用通常配合find_elements做批量校验Link Text / Partial Link TextBy.LINK_TEXT专门给a标签用的按链接文字定位第二类是路径表达式型更灵活但需要更多判断XPathBy.XPATH几乎能定位到页面上任何元素功能最强CSS SelectorBy.CSS_SELECTOR执行效率高语法也简练这里分享一条我总结的实战铁律优先使用ID其次CSS选择器XPath只作为兜底方案。为什么这条优先级顺序很重要因为ID命名稳定且唯一CSS选择器干净快而XPath虽然万金油但一旦写得太长太具体页面前端稍微改一点结构就失效了。看下面这个对比# 不推荐的绝对路径XPath //html/body/div[3]/div[1]/div[2]/form/div[2]/input[1] # 相对可靠的CSS选择器 #login-form input[nameusername] # 更抗变的写法 input#username绝对路径是定位大忌前段加个容器就全挂了。而加了id、name这些属性的CSS选择器哪怕结构调整但关键属性还在脚本依然能跑。Selenium 4还带来了一个好东西相对定位器Relative Locator可以按照元素的相对位置来找元素。比如你要点的按钮在某个文本框的正下方可以这样写from selenium.webdriver.support.relative_locator import locate_with from selenium.webdriver.common.by import By login_btn driver.find_element( locate_with(By.TAG_NAME, button).below(By.ID, username) )这个功能在处理复杂表格、卡片列表时特别好用。3.2 显式等待vs隐式等待不要混用的真相新手最常犯的错误是定位到元素后立刻操作结果报NoSuchElementException。原因很简单页面是异步渲染的你脚本的执行速度远快于页面加载速度元素还没出现脚本就已经去点了。解决办法就是加等待。Selenium提供两种方式隐式等待implicitly_wait和显式等待WebDriverWait。隐式等待是全局的轮询等待设置一次所有定位操作都生效driver.implicitly_wait(10) # 每次find_element最多等10秒但隐式等待有个问题它只解决元素出现的问题解决不了元素出现了但还不可用、被遮挡、状态还没就绪这些问题。而且它和显式等待混用时超时时间容易莫名其妙叠加行为变得很难预测。我现在的实践是项目里只用显式等待不用隐式等待。显式等待的做法是基于特定条件的轮询from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait WebDriverWait(driver, 10) submit_btn wait.until( EC.element_to_be_clickable((By.ID, submit)) ) submit_btn.click()element_to_be_clickable只是众多条件中的一个常用的还有presence_of_element_located元素存在于DOMvisibility_of_element_located元素可见text_to_be_present_in_element元素包含指定文本alert_is_present弹窗出现staleness_of元素已从DOM移除关于等待时间设多少我的经验是不要拍脑袋定一个固定值。如果10秒还不够大概率不是页面慢而是你的定位条件写得太宽等到了错误的元素。把定位条件精确定位到目标元素等待时间2到3秒就足够稳定。3.3 浏览器交互操作注意事项点击、输入、选择这些基础操作本身不难坑都在细节里click()不是万能的。有些元素被遮挡了click会抛ElementClickInterceptedException。这时不要第一反应加等待先检查是不是有遮罩层、加载动画盖住了按钮。绕过方式是用JavaScript直接触发driver.execute_script(arguments[0].click();, element)send_keys之前先清空。输入框里可能默认有内容直接用send_keys会拼接上去。正确流程是先clear()再send_keys()。input_box driver.find_element(By.ID, phone) input_box.clear() input_box.send_keys(13800138000)下拉选择用Select类不要手动点击再选选项。Selenium专门提供了处理select标签的类from selenium.webdriver.support.ui import Select select Select(driver.find_element(By.ID, city)) select.select_by_visible_text(北京) # 按可见文本选 select.select_by_value(bj) # 按value属性选 select.select_by_index(2) # 按下标选文件上传不一定要模拟弹窗。如果页面有input[typefile]的隐藏输入框直接向这个元素send_keys文件路径即可不用去处理操作系统原生弹窗。鼠标悬停和拖拽需要引入ActionChainsfrom selenium.webdriver.common.action_chains import ActionChains ActionChains(driver) .move_to_element(menu_item) # 悬停 .click() .perform() # 拖拽 ActionChains(driver) .drag_and_drop(source, target) .perform()我遇到过很多次菜单是hover才展示的直接用click定位子菜单项总失败这就是典型的时序和交互状态问题用ActionChains做完整的悬停动作链就稳了。4. 完整实操从单条脚本到一个可维护的测试项目4.1 搭建测试工程目录纯粹写一个脚本跑通流程很简单但真实的自动化项目要做的是可持续维护。我建议的目录结构长这样web_auto_test/ ├── config/ │ ├── __init__.py │ └── settings.py # 全局配置URL、登录信息、等待时间 ├── pages/ │ ├── __init__.py │ ├── base_page.py # 页面基类封装公共方法 │ ├── login_page.py # 登录页的定位和操作 │ └── home_page.py # 首页相关操作 ├── testcases/ │ ├── __init__.py │ ├── conftest.py # pytest的fixture驱动初始化/销毁 │ └── test_login.py # 登录功能用例 ├── reports/ # 测试报告输出目录 ├── screenshots/ # 失败截图目录 ├── requirements.txt └── pytest.ini这种分层的价值在项目变大后非常明显页面元素变更了只改pages目录下面对应的文件要新增用例就在testcases里加公共配置和工具都收拢在固定位置不会越改越乱。4.2 conftest里的驱动管理fixturepytest的conftest.py是整个框架的心脏。我在里面做了几件事管理浏览器驱动的启停、自动截图、把driver实例注入测试类。核心代码如下import os import pytest from datetime import datetime from selenium import webdriver from selenium.webdriver.chrome.options import Options from pages.login_page import LoginPage from config.settings import BASE_URL pytest.fixture(scopefunction) def driver(): 每个用例独立浏览器实例保证用例间状态隔离 options Options() if os.getenv(HEADLESS, true) true: options.add_argument(--headlessnew) driver webdriver.Chrome(optionsoptions) driver.implicitly_wait(0) driver.maximize_window() yield driver # 用例结束后自动截图让失败现场有据可查 if hasattr(driver, _test_name): screenshot_dir screenshots os.makedirs(screenshot_dir, exist_okTrue) timestamp datetime.now().strftime(%Y%m%d_%H%M%S) driver.save_screenshot( os.path.join(screenshot_dir, f{driver._test_name}_{timestamp}.png) ) driver.quit()scope设成function意味着每个用例都起一个全新浏览器虽然慢一点但能保证用例之间互不干扰。这是我最推荐的做法除非你非常了解用例之间的依赖关系否则不要图快共用浏览器实例否则一个用例里的操作残留会让下一条用例摸不着头脑。4.3 页面对象模式POM落地POM的核心思想很简单把每个页面抽象成一个类页面上的元素定位器和操作行为都封装在类里测试用例只关心业务操作不关心底层怎么定位元素。base_page.py封装公共能力from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: def __init__(self, driver): self.driver driver self.wait WebDriverWait(driver, 10) def find(self, locator): 统一封装查找元素的方法 return self.wait.until( EC.visibility_of_element_located(locator) ) def click(self, locator): 安全点击确保可点击后再点击 element self.wait.until( EC.element_to_be_clickable(locator) ) element.click() def send_keys(self, locator, text): element self.find(locator) element.clear() element.send_keys(text)login_page.py业务化操作from selenium.webdriver.common.by import By from pages.base_page import BasePage class LoginPage(BasePage): username_input (By.ID, username) password_input (By.ID, password) login_button (By.ID, loginBtn) error_message (By.CLASS_NAME, error-msg) def login(self, username, password): self.send_keys(self.username_input, username) self.send_keys(self.password_input, password) self.click(self.login_button) def get_error_message(self): return self.find(self.error_message).text类属性里放的是元组形式的定位器用By.XXX定义类型这种写法好处是定位器一眼可见集中管理而且可以很方便地做参数化。测试用例就变得非常干净class TestLogin: def test_login_success(self, driver): login_page LoginPage(driver) login_page.open(BASE_URL) login_page.login(demo_user, pass123) assert 欢迎 in login_page.get_welcome_text() def test_login_wrong_password(self, driver): login_page LoginPage(driver) login_page.open(BASE_URL) login_page.login(demo_user, wrong_pass) assert login_page.get_error_message() 用户名或密码错误为什么POM是自动化领域的主流实践因为它把变化的因素控制住了。前端改版是这个行当最频繁的事如果每个用例里都散落着底层定位代码改一次样式要动几十个文件用POM后改一个页面类就够了。这是我在项目里体验最深刻的一点。4.4 数据驱动和参数化被测功能往往有很多等价类场景。登录测试不会只试一组用户名密码需要覆盖正常登录、密码错误、用户不存在、账号锁定、密码为空等等。直接在用例里写死数据会让用例膨胀正确做法是用pytest的参数化import pytest from pages.login_page import LoginPage pytest.mark.parametrize(username,password,expected_msg, [ (, pass123, 请输入用户名), (demo_user, , 请输入密码), (demo_user, wrong, 用户名或密码错误), (not_exist, 123456, 用户不存在), ]) def test_login_validation(driver, username, password, expected_msg): login_page LoginPage(driver) login_page.open(BASE_URL) login_page.login(username, password) assert login_page.get_error_message() expected_msg再复杂一点可以把数据抽到yaml或excel文件里从文件读取来构造参数这就是完整的数据驱动。但对多数项目来说pytest的parametrize装饰器已经够用了不要过度设计。4.5 断言与测试报告让结果一眼看懂自动化测试真正有价值的地方不是跑完而是结果能清楚告诉团队哪里出了问题。我的建议是断言要精确信息要丰富。比如校验页面跳转不要只断言当前URL等于预期值还可以同时检查URL和页面标题def test_redirect_after_login(self, driver): login_page LoginPage(driver) login_page.open(BASE_URL) login_page.login(demo_user, pass123) wait WebDriverWait(driver, 10) wait.until(EC.url_contains(/dashboard)) assert /dashboard in driver.current_url assert 工作台 in driver.title报告方面pytestpytest-html的组合够用且简单pip install pytest pytest-html pytest testcases --htmlreports/result.html --self-contained-html执行完后会生成一个独立的HTML文件浏览器打开能看到每条用例的通过/失败状态失败还有堆栈信息。加上conftest里的截图逻辑每一条失败用例都会附带现场截图调试效率大幅提升。5. 常见问题排查实录那些年踩过的坑5.1 元素找不到了第一步不是改定位器NoSuchElementException应该是自动化测试里最常见的报错。很多人的第一反应是换定位器换个XPath再试结果越改越复杂。我的排查套路是固定的第一步检查页面到底加载出来没有。手动打开同样的环境看看是不是登录之后还有跳转、弹窗或者异步加载延迟。如果页面都没到那问题不在定位器在流程时序。第二步确认元素是否存在只是不可见。可以在调试时执行driver.execute_script(return document.body.innerHTML)看页面内容或者用浏览器的开发者工具手动查询元素是否在DOM里。第三步检查是否在iframe里。这个坑太经典了页面看似平铺的元素其实嵌套在iframe里主文档的查找器根本够不着它必须先切换上下文driver.switch_to.frame(iframe_id) # 按ID切换 driver.switch_to.frame(driver.find_element(By.NAME, iframe)) # 按元素切换 # 操作完成后回到主文档 driver.switch_to.default_content()我之前排查过一个在线编辑器的问题所有富文本元素都定位不到折腾一整天才发现整个编辑器都嵌在iframe里。这个知识点如果不提前了解遇到会很崩溃。第四步检查是不是新窗口。点击某个链接后开了新标签页driver还没切过去你说能找到才怪original_window driver.current_window_handle driver.switch_to.new_window(tab) # Selenium 4写法 # 或者切到已打开的窗口 for handle in driver.window_handles: if handle ! original_window: driver.switch_to.window(handle) break5.2 StaleElementReferenceException引用过期问题这个异常的意思是你之前拿到的元素对象现在指向的DOM节点已经不存在了。典型场景是页面做了局部刷新、列表数据重新渲染、翻页操作之后旧引用自然失效。解法也很直接重新获取元素。最稳妥的方式是不要长期持有元素引用每次操作前都通过定位器重新拿也就是我们POM里的find方法干的事。另外加显式等待等刷新完成再获取新引用。5.3 测试不稳定时好时坏最让人崩溃自动化跑的用例有时候全过有时候随机挂这是最磨人的。结合经验测试不稳定主要来自三个源头一是时序问题。某个操作完成后依赖的渲染还没结束被固定等待蒙混过去了。等3秒没事等2秒就挂。这种问题的正解是用显式等待替代固定等待等待业务层面的结果出现而不是靠运气。二是测试数据污染。A用例创建了一个同名账号B用例创建同账号就失败。解决方案是每条用例使用独立唯一的数据时间戳拼接是常见做法import time unique_username ftest_{int(time.time())}三是环境变化。不同环境测试/预发布配置不同有的有Mock有的接真实服务。写用例时尽量避免对业务数据的强依赖优先使用可以动态创建的测试数据。5.4 headless模式下的一些奇怪问题headless模式省资源但偶尔会有一些奇怪的现象页面布局不一样、某些控件展示异常、截图是空白的。我的建议是本地调试一律用有头模式看清楚到底在跑什么只有在CI服务器上才开headless。另外我遇到过headless模式下截图是白色空白页的情况多半是页面还没渲染完成就截图了。截图前务必加一个等待条件确认页面核心元素已经可见。5.5 常见问题速查表把高频问题整理成一个表方便直接查报错或现象最常见原因排查顺序NoSuchElementException元素未渲染/定位器错误/iframe先看页面是否加载完再看iframeElementClickInterceptedException有遮罩层/动画遮挡等待动画结束或JS直接点击StaleElementReferenceException页面刷新元素引用过期重新获取元素引用TimeoutException等待条件一直不满足检查定位器是否精确检查业务是否真的在进行时好时坏时序/数据污染/环境差异从等待、数据、环境三个方向排查无法定位动态id的元素id每次加载都会变改用相对定位或者XPath的contains函数定位动态id这类问题展开说一下。很多前端框架会生成动态属性比如id是uuid或者自增数字这种情况下就不要死磕动态属性而是# 用contains匹配稳定前缀 xpath //input[contains(id, username)] # 或者用层级关系稳定定位 xpath //div[classlogin-form]//input[typepassword]我这里只是用了contains这种最基础的XPath函数但它解决了很多实际场景。不过还是提醒一句能用CSS选择器解决的不要硬上XPath保持选择器简单的风格。6. 一些个人的实践体会说实话做Web自动化测试几年我最深的体会是自动化测试的核心难点从来不在于Selenium这个工具本身而在于你怎么设计用例、怎么管理数据、怎么控制环境。工具只是手段好的框架设计才能让自动化资产真正活下来。如果你刚开始接手一个新项目的自动化测试我建议从最关键的核心流程入手比如登录、主业务流程、关键表单提交这几个冒烟用例先跑通再扩展。不要一上来就追求覆盖所有功能那样脚本质量和稳定性都会很糟每天维护脚本就把人耗干了。还有一点很重要自动化测试的价值在于回归不在于找bug。UI自动化的定位是防止已知问题在改版和迭代中复现。想通过UI自动化发现新缺陷不如去做接口测试和单元测试效率高得多。这个定位想清楚了你就知道哪些用例该自动化哪些不值得。最后分享一个排查定位器的小技巧调试阶段可以在浏览器开发者工具的Console里用document.querySelector验证你的CSS选择器是否命中了预期元素比如document.querySelector(#loginBtn)如果能返回元素说明定位器语法和页面结构是匹配的问题大概率出在时序而不是定位器本身。这个技巧帮我少加了很多无用代码。Selenium 4后续还有更多新特性值得挖掘比如几个新的定位思路、增强的Action API、对Shadow DOM更好的支持。React和Vue这类现代前端项目里Shadow DOM越来越多遇到自定义组件内部元素定位不到时往这个方向研究比硬写XPath靠谱得多。工具会迭代但底层的基础功——定位思路、等待策略、POM设计——掌握了换什么框架都是相通的。

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

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

免费获取报价 →
↑