ARTICLE DETAIL

资讯详情

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

Selenium测试框架快速搭建:从脚本到稳定自动化体系

Selenium测试框架快速搭建:从脚本到稳定自动化体系 最近好几个朋友问我同样的问题Selenium的脚本明明能跑为什么一多起来就乱成一锅粥还有人直接说想快速搭一套selenium测试框架但网上资料要么太散、要么直接甩一个几十个文件的企业级项目根本不知道从哪下手。我自己的经历也差不多。早先用Selenium写Web自动化时也是从一堆能跑的脚本起步的跑到五十个用例左右就坚持不住了——元素定位改一处脚本崩一片浏览器一更新全部罢工最要命的是第二天看失败报告根本不知道当时页面上发生了什么。后来花时间把框架重新理了一遍才发现真正需要的东西并不多。这篇就围绕selenium测试框架快速搭建这件事把我验证过的搭建路径完整过一遍从脚本和框架的边界、浏览器驱动的版本匹配到目录结构、Page Object、等待与定位、报告与失败截图最后是几个实战里踩出来的坑。不追求大而全追求的是今天看完、明天就能开工的水平。适合刚接触Web自动化测试的测试工程师也适合想把手头散装脚本整理成体系的后端开发。1. 先别急着写代码脚本和框架的边界在哪1.1 一堆能跑的脚本不等于一套框架很多人把框架理解成很多脚本。实际上区别不在数量而在应对变化的能力。一个简单的自动化脚本大概是这个模式打开浏览器、访问地址、找元素、点按钮、填表单、断言、关闭浏览器。单个脚本跑通很简单难的是业务一变你能改多快页面一重构你能定位多准跑完一百个用例你能不能三分钟之内告诉团队哪些功能是好的哪些坏了。我见过最典型的反例在一个项目的tests目录下堆了两百多个py文件每个文件都是从头启动driver每个文件里都写着time.sleep(3)等加载。表面上是自动化测试代码实际上是个定时炸弹——谁动了公共页面结构谁就要把所有文件翻一遍。框架要解决的核心问题归纳起来就三件事稳定反复跑结果一致、可维护页面改了只动一处、可观测失败了知道为什么。1.2 快速搭建的合理预期快速搭建不是让你半天搞出一个企业级平台。我给自己定的标准是第一天目录结构立起来能跑通一个冒烟用例第二天公共封装和Page Object铺开补上等待策略和失败截图第三天报告能看失败能定位可以开始批量写业务用例三天是可用的起点之后才是持续扩展。如果一开始就想把数据驱动、关键字驱动、多环境切换、分布式执行全塞进来大概率一周过去还在搭架子。明确这个预期很重要。因为测试框架这个词被很多人神话了好像不搞出一堆抽象类、装饰器和插件体系就不够专业。实际情况恰恰相反超过80%的Web自动化项目用目录规范 公共封装 Page Object pytest这一套完全够用。2. 环境准备的头号陷阱浏览器驱动版本匹配2.1 为什么Selenium不能直接驱动浏览器新手问得最多的一个问题为什么我装好了selenium库却启动不了浏览器这里要先理解Selenium的架构。Selenium本身不是直接操作浏览器的工具它通过WebDriver协议一套HTTP接口规范同一个独立的可执行程序通信这个可执行程序就是浏览器驱动。Chrome对应chromedriverFirefox对应geckodriverEdge对应msedgedriver。驱动收到Selenium发来的命令后再通过浏览器的调试接口去操作真实浏览器。所以环境准备环节有三件套编程语言环境Python、selenium库、浏览器驱动。缺一不可驱动版本不匹配也不行。2.2 版本匹配到底怎么判断这是很多新手反复踩坑的地方web自动化selenium浏览器驱动怎么判断下载哪个区别。答案其实很直接Chrome浏览器和ChromeDriver必须保持主版本号一致主版本号一致后驱动可以选与浏览器版本相同或略新的版本尽量别选旧一个主版本的免得API对不上具体操作打开Chrome点右上角三个点 → 帮助 → 关于Chrome看到类似版本 119.0.6045.200这样的信息记下第一位数字119去ChromeDriver官方下载页chromedriver.chromium.org找对应版本号目录比如119开头的目录根据操作系统选linux / mac / win对应的压缩包如果浏览器版本很新官方目录里还没同步这时候别急着降低浏览器版本来迁就驱动。有两条路一是用下面的WebDriver Manager工具让它自动匹配二是去Chrome for Testing的通道找匹配版本。2.3 推荐做法用webdriver-manager管理驱动手动下载驱动的问题很明显每次Chrome一更新你的自动化环境大概率就挂了。团队里多几台机器每台都要手动配一遍纯浪费生命。更好的办法是用webdriver-manager这个库自动检测本地浏览器版本、自动下载匹配的驱动、自动缓存。安装就是一行pip install selenium webdriver-manager用起来是这样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) driver.get(https://example.com)它会先读取本机Chrome的版本号然后去托管地址下载对应的ChromeDriver并放到本机缓存目录。后续再跑如果版本没变就直接用缓存不会重复下载。Firefox用户对应的是GeckoDriverManager()Edge用户是EdgeChromiumDriverManager()用法完全一样。如果公司网络访问外网比较慢可以提前在CI机器上跑一次把驱动缓存进镜像之后就跑本地缓存。提示如果司内网络限制下载可以手动把驱动放到固定目录比如项目根目录的drivers/然后用Service(drivers/chromedriver.exe)指向它。这种做法虽然要手动维护但在封闭网络环境里反而最省事。3. 搭骨架目录分层、公共封装与第一个用例3.1 一个能撑住业务扩展的目录结构框架的价值很大程度体现在目录结构上。结构清晰团队里任何人都能在三十秒内找到页面对象放哪、用例放哪、公共方法放哪。我推荐的目录结构麻雀虽小五脏俱全project/ ├── config/ │ └── settings.py # 全局配置URL、超时、浏览器类型 ├── core/ │ ├── base_page.py # BasePage所有页面对象的父类 │ └── driver_factory.py # 浏览器驱动创建与回收 ├── pages/ │ ├── login_page.py # 登录页的页面对象 │ └── home_page.py # 首页的页面对象 ├── testcases/ │ ├── conftest.py # pytest fixture全局setup/teardown │ └── test_login.py # 登录相关用例 ├── utils/ │ ├── log_util.py # 日志封装 │ └── screenshot_util.py # 截图封装 ├── reports/ # 测试报告和失败截图输出目录 ├── requirements.txt └── pytest.ini这个结构没有引入复杂的框架层级但对一个中型Web项目来说分工已经足够了。核心逻辑是按职责分层config管配置core管公共能力pages管页面结构testcases管业务用例utils放工具函数。各层之间的依赖关系是单向的testcases依赖pages和corepages依赖corecore不依赖业务。这样设计的目的就是当某个页面的结构变了你只需要动pages里对应的一个文件公共方法变了只需要动core业务用例尽量不动。3.2 先写driver_factory统一管理浏览器生命周期driver创建看起来就是一行webdriver.Chrome()但真正项目里要处理的细节不少要不要无头模式、要不要最大化、加载失败怎么办、用例结束后能不能确保退出。我习惯用driver_factory统一处理这些逻辑# core/driver_factory.py from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager def create_driver(headlessFalse): options webdriver.ChromeOptions() if headless: options.add_argument(--headlessnew) options.add_argument(--window-size1920,1080) # 忽略证书错误常见于测试环境 options.add_argument(--ignore-certificate-errors) # 禁用自动化控制提示减少页面波动 options.add_argument(--disable-blink-featuresAutomationControlled) # 避免Chrome在某些CI容器里直接crash options.add_argument(--no-sandbox) service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice, optionsoptions) driver.implicitly_wait(5) driver.maximize_window() return driverheadless用得多的场景是CI和批量回归。注意headless模式下有些页面的渲染逻辑和正常模式不完全一致比如某些依赖窗口尺寸的布局所以我会固定一个1920x1080的窗口大小减少这类差异。3.3 pytest.ini和conftest.py把基础设施一次配好pytest是Python生态里最顺手的测试框架之一。配合Selenium用主要靠fixture机制来管理浏览器生命周期——测试函数执行前自动启动浏览器执行后自动关闭中间失败还能自动截图。pytest.ini里最少要配这些[pytest] testpaths testcases python_files test_*.py python_classes Test* python_functions test_* addopts -v --tbshortconftest.py是整个fixture的注册中心。一个最小可用的版本# testcases/conftest.py import pytest from core.driver_factory import create_driver pytest.fixture(scopefunction) def driver(): 每个用例独立的浏览器实例确保用例互不影响 _driver create_driver() yield _driver _driver.quit()scopefunction意味着每个用例都会新建浏览器。跑大数据量用例时效率低一些但换来了用例之间的隔离性——这是自动化测试稳定性的第一原则。等用例数量上来后再考虑用scopemodule或session做浏览器复用来加速。写到这里第一个用例已经可以跑了# testcases/test_login.py from pages.login_page import LoginPage def test_login_success(driver): page LoginPage(driver) page.open() page.login(tester, 123456) assert 首页 in page.get_title()这个用例看着简单但背后已经有了配置、驱动管理、页面对象和pytest的调度。接下来要做的是把页面对象这一层做扎实。4. 页面对象模型把元素定位从业务里剥出去4.1 为什么必须做Page ObjectPage Object ModelPOM是Web自动化最具通用价值的设计模式没有之一。核心思想很朴素把页面上所有元素定位和操作方法封装成一个类用例代码只跟这个类交互不直接写find_element。好处随手能举出三个页面HTML结构变了只改页面类用例一行不用动元素定位字符串不会散落在各个用例里避免重复页面上的操作意图登录、搜索、加购物车有了统一入口用例读起来像业务文档老手和新手写自动化最大的分水岭就在这一点上。4.2 BasePage把重复逻辑收拢每个页面对象都会用到等待、点击、输入、截图这些能力。如果每个页面类都自己实现一遍就是大量重复代码。所以先写一个BasePage把公共能力沉淀下来# core/base_page.py from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: def __init__(self, driver: WebDriver): self.driver driver self.wait WebDriverWait(driver, 10) def open(self, url): self.driver.get(url) 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 input_text(self, locator, text): element self.find(locator) element.clear() element.send_keys(text) def get_title(self): return self.driver.title def take_screenshot(self, name): path freports/screenshots/{name}.png self.driver.save_screenshot(path) return path注意click方法这里用的是element_to_be_clickable而不是简单的find后.click。原因很现实页面上很多按钮在加载动画、遮罩层还没消失时虽然已经出现在DOM里但点击会被挡住直接.click就会偶发失败。用可点击等待能过滤掉一大部分这种不稳定因素。4.3 一个实际的页面对象长什么样以最常见的登录页为例# pages/login_page.py from selenium.webdriver.common.by import By from core.base_page import BasePage class LoginPage(BasePage): username_input (By.ID, username) password_input (By.ID, password) login_button (By.CSS_SELECTOR, button[typesubmit]) error_msg (By.CLASS_NAME, error-tip) def open(self): super().open(https://example.com/login) def login(self, username, password): self.input_text(self.username_input, username) self.input_text(self.password_input, password) self.click(self.login_button) def get_error_message(self): return self.find(self.error_msg).text类属性直接定义定位元组是POM里非常常见也够用的写法。定位符集中在类顶部一眼就能看到这个页面依赖哪些元素后续维护非常友好。如果页面大还可以把定位符单独抽成一个locators.py模块不过对大多数项目来说类顶部已经够了。用例侧就变成了很干净的调用def test_login_success(driver): LoginPage(driver).login(tester, 123456) assert HomePage(driver).get_welcome_text() 欢迎回来tester这时候你再对比文章开头说的两百个散装脚本的场景登录功能出问题时你只需要打开pages/login_page.py检查定位符而不是在IDE里全局搜索一百处。5. 跑稳的关键等待策略与定位方式的实用组合5.1 别再用time.sleep硬等了这是我最想按住每一位新人肩膀提醒的事自动化脚本的90%不稳定都源自不合理的等待。time.sleep(3)的问题不用多说固定等三秒页面两秒就加载完了白白浪费一秒网络慢了要五秒你又等不够。它既慢又不稳。正确姿势分两种隐式等待implicitly_wait告诉WebDriver在查找元素时如果元素没有立即出现最多等待N秒再抛异常。它作用于整个driver生命周期内所有find_element调用代码只需写一次。显式等待WebDriverWait针对特定条件在N秒内反复尝试直到条件满足或超时。更精准也更可控。实际项目里两者可以同时配置但隐式等待要设成兜底水平我习惯5秒真正关键的交互操作用显式等待设10秒以上。注意不要对同一段等待过程混用隐式和显式逻辑。Selenium的官方文档明确说过混用会导致等待时间变成两者的叠加出现莫名其妙等了很久的问题。简单说全局隐式等待设一个保守值关键步骤用显式等待不额外叠加。5.2 定位方式的选择优先级Selenium提供了ID、Name、Class Name、Tag Name、CSS Selector、XPath、Link Text/Partial Link Text共八种定位。实际工作中我遵循的优先级是这样的优先级定位方式适用场景风险点1ID元素有稳定ID最常见前端重构可能改ID2CSS Selector复杂页面结构无ID选择器写太长会脆3XPath文本定位、动态属性绝对路径极容易挂4Name表单字段同名name多时无法用5Class Name单一样式类容易误命中多个元素6Link Text链接文字页面文案调整就废我的原则是能用ID就不用CSS能用CSS就不用XPath。XPath本身并不差很多复杂场景比如包含某文本的按钮它就是最优解。真正坑人的是浏览器插件自动生成的绝对路径XPath形如/html/body/div[2]/div[3]/div/div[1]/form/div[2]/button这种路径一级级数下来加到第七八层基本就是一次性用品了。相对XPath和CSS是有讲究的比如# CSS找class包含submit的按钮 submit_btn (By.CSS_SELECTOR, button[class*submit]) # XPath找文本为确认的按钮 confirm_btn (By.XPATH, //button[text()确认]) # XPath找name属性以user开头的输入框 user_input (By.XPATH, //input[starts-with(name, user)])这些写法比绝对路径坚韧得多页面布局微调时一般不会挂。5.3 iframe、新窗口和阴影DOM最容易翻车的三个场景这三个场景几乎每个做Web自动化的人都会碰到。iframe元素明明在页面上find却报找不到。很可能是元素在iframe里WebDriver默认只会找当前文档里的元素。处理方式# 先切换到iframe iframe driver.find_element(By.CSS_SELECTOR, iframe[idpayment]) driver.switch_to.frame(iframe) # 操作完成后切回主文档 driver.switch_to.default_content()新窗口点击一个链接弹出了新标签页你以为还是原来那个窗口结果怎么都找不到元素。处理方式# 记住当前窗口句柄 original_window driver.current_window_handle # 点击触发新窗口... # 等待新窗口出现切换进去 WebDriverWait(driver, 10).until(EC.number_of_windows_to_be(2)) new_window [w for w in driver.window_handles if w ! original_window][0] driver.switch_to.window(new_window)阴影DOM很多现代组件库比如某些日期选择器、上传组件内部用的是自定义元素Shadow DOM。普通find_element是穿透不了的需要先获取shadow rootshadow_host driver.find_element(By.CSS_SELECTOR, my-custom-element) shadow_root driver.execute_script(return arguments[0].shadowRoot, shadow_host) inner_button shadow_root.find_element(By.CSS_SELECTOR, button.confirm)这三个场景都属于报错信息看得见但原因不好猜的类型。遇到find找不到元素先按这个顺序排查是不是iframe、是不是新窗口、是不是Shadow DOM、是不是还在加载。90%的找不到元素都能归到这几类。6. 让框架看得见报告、失败截图与日志6.1 pytest-html还是allure怎么选框架跑起来后下一个问题就是结果怎么呈现。没人愿意对着控制台看200行日志来确认测试通过没通过。pytest-html和allure是目前最主流的两个选择。我的建议很简单项目刚开始用pytest-html团队对报告有更高要求再切allure。两者的区别维度pytest-htmlallure安装复杂度低pip装完即用较高需要装allure命令行工具报告样式简洁实用更美观分类清晰历史趋势无有趋势图和分类统计与CI集成简单需要allure插件上传结果适合阶段快速落地团队级规范管理pytest-html的接入几乎零成本pip install pytest-html运行用例时加参数pytest testcases/ --htmlreports/report.html --self-contained-html--self-contained-html会把CSS和JS都打进单个HTML文件里方便邮件发送或存档。没这个参数的话报告目录里会散落一堆资源文件拷贝给别人就容易丢样式。6.2 失败自动截图用pytest钩子实现报告再漂亮如果没有失败现场的截图和日志排查效率依然很低。所以我会在conftest.py里加一个pytest的钩子用例失败时自动截图、自动记录页面标题和当前URL# testcases/conftest.py import pytest from datetime import datetime 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: ts datetime.now().strftime(%Y%m%d_%H%M%S) name f{item.name}_{ts} path freports/screenshots/{name}.png driver.save_screenshot(path) report.sections.append((截图, f保存至{path}))这个钩子的作用范围是pytest整个执行管线tryfirstTrue是为了让我们的逻辑在别的插件处理之前拿到结果。任何用例失败都能自动留下案发现场不用在每个用例里手动写try/except。配合日志我还会把页面关键操作log下来比如点击了登录按钮、输入了用户名tester。日志级别不用太细关键是失败时能还原操作序列。utils/log_util.py可以用Python自带的logging模块搞定半小时就能配好。6.3 headless模式与CI的接入框架基本功能齐了之后做CI是水到渠成的事。CI上跑自动化通常用headless模式把浏览器无头运行省资源、不需要显示环境。接入方式很简单在driver_factory里加一个环境变量开关import os def create_driver(): headless os.getenv(HEADLESS, 0) 1 ...CI配置里设HEADLESS1本地调试默认不设。同一套代码本地肉眼调试和CI无人值守两不误。我的经验是CI上跑用例重点盯两件事。第一是稳定性用例失败率长时间高于5%先别急着加用例先查是不是等待或定位问题第二是报告归档把pytest-html生成的report.html和screenshots目录设成CI的artifacts失败后直接从CI下载即可定位。7. 最后说说那些只能在实战里踩出来的教训7.1 用例之间的隔离性比执行速度重要得多我早期为了追求快用了一个session级的driver所有用例共用一个浏览器。跑起来确实快但用例之间的状态也串了上一个用例的登录状态还在下一个用例的断言就挂了上一个用例页面滚动位置不对下一个用例点击就偏了。后来老实改回function级driver一个用例一个浏览器实例虽然慢了一些但整个套件的稳定性翻了几倍。执行速度的问题应该用并行执行pytest-xdist去解决而不是牺牲隔离性。7.2 定位符维护要当代码来管理定位符最容易出现的问题是页面没变但定位符自己长毛了——某天某个人为了临时绕过一个问题把ID定位改成了XPath绝对路径另一个人又给某条click加了sleep。这些看似微小的改动会在一个月后集中引爆。所以我在项目里有一条不近人情的规矩所有定位符必须集中放在页面对象的类属性里任何人不得在用例文件里出现By.ID或find_element。代码评审时这条会被重点检查。坚持两三个迭代之后定位符的混乱程度会明显下降。7.3 真实项目里最常碰到的几个幽灵问题最后整理一份高频问题清单都是我自己和同事在项目中真实遇到过的现象常见根因解决方向本地能过、CI上挂窗口尺寸、字体、网络速度差异headless固定窗口大小等待放宽偶发点击无效遮罩层、动画未结束、按钮被覆盖用element_to_be_clickableJS点击兜底第二天全挂前端半夜发版元素属性变了定位符版本化管理发版前跑回归断言不准页面还有异步数据未加载完成断言前显式等待目标文本出现内存占用越来越大用了session级driver没清理确保quit在fixture中执行其中偶发点击无效最值得多说一句。元素看起来能点到但点击后没有任何反应大概率是某个透明遮罩层盖在上面。遇到这种情况先执行JS把它移掉再点击def safe_click(self, locator): element self.wait.until(EC.element_to_be_clickable(locator)) self.driver.execute_script(arguments[0].scrollIntoView(true);, element) try: element.click() except Exception: self.driver.execute_script(arguments[0].click();, element)先尝试正常点击失败就退回JS直接触发click。这是我在大量页面元素被覆盖的现实环境里反复验证过的最稳健的点击方案。当然JS点击绕过了用户真实操作的一些前提校验尽量只在正常点击失败时兜底使用。7.4 一套框架的终点不是代码写完框架搭完、报告有了、CI接上了这件事其实才完成一半。真正让框架持续发挥价值的是后面几个动作每个迭代安排固定的回归窗口把新增用例纳入日常每次前端发版前先跑一遍自动化套件定位符或页面结构变更时记录变更原因方便后来人理解。做了几年Web自动化我的体感是框架的价值从来不在代码量和技术名词而在团队能不能依赖它快速回答这次改动有没有弄坏功能这个问题。能回答哪怕目录结构朴素一点、用的全是基础API也是一套好框架回答不了堆再多高级组件也是自嗨。如果你正准备搭第一套selenium测试框架照着上面这些步骤先把骨架立起来跑两个真实业务用例然后让它在日常迭代中慢慢长出细节。遇到定位不稳定就优化等待遇到报告不好看就调整输出遇到用例太多跑得慢就接并行。框架是长出来的不是一下子设计出来的。
返回列表