资讯动态

Python+Selenium+Pytest:从零搭建WebUI自动化框架

发布时间:2026/10/14 1:23:43 来源:尧图企业网站定制
1. 从“点按钮”到“跑脚本”我为什么建议你自己搭一套WebUI自动化框架先说结论如果你所在的项目已经稳定迭代了半年以上UI层面的回归测试还靠人工一周点一遍这套从零搭建的思路一定值得你看完。我自己在这个领域踩过不少坑。早期做测试的时候拿到一个Web项目第一反应就是写脚本。把登录、下单、查询这些主流程用工具录一遍回放能用就觉得自己“会自动化”了。但真正跑到第二个迭代就发现用例之间互相依赖页面稍微改个按钮位置脚本就全红定位方式千奇百怪连个统一的报告都没有。后面才明白自动化测试真正难的不是写脚本而是搭框架——把脚本的组织方式、执行方式、数据管理方式、结果反馈方式统一起来。这也是“从无到有搭建WebUI自动化测试框架”这个选题的核心价值。这篇博文会从一个空目录开始带你完整走一遍技术选型、目录结构、驱动管理、Page Object封装、数据驱动、报告与持续集成最后聊几个我实测中遇到的典型坑。整个过程不依赖任何商业平台全部基于Python开源生态完成适合有一定Python基础、想系统搭建WebUI自动化体系的测试开发同学参考。2. 框架搭建前的三个关键选型为什么是PythonSeleniumPytest2.1 Python在UI自动化生态里的位置WebUI自动化在2020年前后经历了一轮明显的技术分化。主流路线主要有三派一是以Selenium为代表的老牌WebDriver方案二是以Playwright为代表的新一代浏览器自动化方案三是Cypress这类偏向JS生态的框架。但如果限定“Python”这个前提Selenium配合Pytest依然是覆盖面最广、踩坑资料最多、对新手最友好的一套组合。选择Python做底层还有一个现实原因测试团队内部往往不是所有人都是专业开发Python的语法门槛低写出来的用例代码团队评审也容易通过。更重要的是Python在数据处理、接口调用、甚至后续的AI辅助测试脚本生成上都有天然的生态优势框架未来想扩展能力不需要切换语言。2.2 Selenium 4为什么仍然是稳妥的底座Selenium 4相比3.x版本在W3C WebDriver协议上做了完整落地驱动与浏览器的通信更标准。相对旧版本的很多“黑魔法”操作新版本在跨浏览器支持、等待策略、窗口管理上都规范了很多。实际项目中我见过不少人纠结Selenium已经被Playwright超越是不是不该选了我的观点是技术选型要考虑团队维护成本和历史资产。如果你的项目里已经有几百条基于Selenium的用例迁移到Playwright重写这些用例的代价远大于Selenium本身带来的效率损耗。而如果是全新项目Selenium依然有一份很大的优势——所有的开源方案、博客案例、社区问答你几乎都能找到对应的解决方案排查问题效率极高。2.3 Pytest从断言到夹具一个被低估的测试框架很多初学者写自动化用例时习惯直接用unittest。但Pytest有几个特性是WebUI自动化场景里非常实用的Fixture机制可以非常优雅地实现浏览器实例的复用与回收。参数化一条用例跑多组测试数据是数据驱动的核心支撑。插件生态配合pytest-html、pytest-xdist、pytest-rerunfailures报告、并发、重试全部解决。断言简洁原生assert语法写起来比unittest的assertEqual更接近自然语言。我经常打一个比方unittest像一把螺丝刀能拧螺丝Pytest是一整套工具箱拧螺丝只是其中一项功能。框架搭建如果只选一个核心组件Pytest是优先级最高的。3. 框架的目录设计与核心模块划分3.1 一个可以直接抄的目录结构框架搭建最忌讳的事情是想到哪写到哪。我见过最混乱的项目所有脚本都在一个文件夹里文件名从test1.py排到test99.py没人知道哪个是能跑的。一个合理的框架从目录上就能看出设计思路。下面这个结构是我经过多个项目迭代后确定的适用性最强webui_auto_framework/ ├── config/ # 配置文件存放处 │ ├── setting.py # 全局参数base_url、超时时间、浏览器类型 │ └── data.yaml # 环境相关的数据配置 ├── data/ # 测试数据 │ ├── login_data.json # 登录模块的测试数据 │ └── order_data.yaml # 订单模块的测试数据 ├── drivers/ # 浏览器驱动存放 │ ├── chromedriver_win64/ │ ├── chromedriver_mac/ │ └── geckodriver/ ├── logs/ # 运行日志 ├── reports/ # 测试报告 │ ├── html/ # pytest-html生成的结果 │ └── screenshots/ # 失败截图 ├── pages/ # Page Object对象层 │ ├── base_page.py # 所有页面的基类 │ ├── login_page.py │ └── home_page.py ├── testcases/ # 测试用例层 │ ├── conftest.py # Pytest夹具定义 │ ├── test_login.py │ └── test_order_flow.py ├── utils/ # 工具模块 │ ├── logger.py # 日志封装 │ ├── driver_manager.py # 浏览器驱动管理 │ ├── yaml_reader.py # 配置文件读取 │ └── screenshot.py # 截图工具 ├── requirements.txt └── pytest.ini这套目录强调两点一是分层清晰——页面对象、用例、数据、配置互不干扰二是扩展友好——新增加一个模块的功能测试只需要在pages和testcases各新增一个文件不需要改动已有的任何逻辑。3.2 为什么选择Page Object模式作为页面封装的核心思路Page ObjectPO是WebUI自动化里最经典的封装思想核心做法是为每一个页面建立一个类把页面的元素定位和操作行为都封装到类内部用例层只需要调用这些方法不需要关心元素长什么样。举个现实例子。登录页有一个用户名输入框如果不用PO你的用例代码里到处会出现driver.find_element(By.ID, username).send_keys(tester01)这个写法最致命的问题是当开发把username改成loginName你需要全文搜索所有出现这个定位的地方逐一修改。如果有100条用例涉及登录就是100次修改。用了PO之后定位代码只存在于login_page.py一处class LoginPage(BasePage): username_input (By.ID, username) password_input (By.ID, password) login_button (By.ID, login_btn) def input_username(self, username): self.find_element(self.username_input).send_keys(username) def login(self, username, password): self.input_username(username) self.input_password(password) self.click_login()以后元素变了只需要改这个文件。这就是页面封装的核心收益——把“变”的部分收拢到一处让用例层的代码保持稳定。这个思路跟代码里用函数封装重复逻辑是同一个道理只不过隔离的粒度是“页面”。3.3 BasePage基类里该放什么BasePage是整个PO层的底座我通常会放以下几类内容元素查找的二次封装支持显式等待后再查找常用操作点击、输入、清空、获取文本页面通用判断元素是否存在、是否可见截图与日志的记录入口JavaScript操作的封装滚动到指定元素、强制点击隐藏元素以显式等待为例Selenium原生写法每次都要写WebDriverWait(driver, 10).until(...)一段两行能解决的代码要写五行。封装到BasePage后def find_element(self, locator, timeout10): return WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located(locator) )用例层调用时只关心“我要找什么元素”不关心“等多久、怎么等”。封装本身就是一种对复杂度的隔离BasePage做得越完善后续的页面类写起来就越顺畅。4. 浏览器驱动的自动化管理方案4.1 为什么手动下载驱动不靠谱早期做Selenium自动化最困扰的问题之一是驱动版本与浏览器版本不匹配。Chrome浏览器升级到某个版本后chromedriver如果还是旧版的脚本大概率报session not created或者unknown error: cannot find Chrome binary。手动跑去镜像站下载驱动还要看一眼Chrome版本号再选择对应驱动版本这个流程每个成员都要经历一遍而且一旦浏览器自动更新驱动又失效。后来我找到了更优的解决方案——使用webdriver-manager库。这个库会自动检测本机浏览器版本自动下载对应的驱动文件并且支持缓存放包每次执行前会自动判断是否需要更新。在requirements.txt里加上selenium4.15.0 webdriver-manager4.0.1 pytest7.4.0 pytest-html4.1.0 pytest-rerunfailures13.0 pyyaml6.0.1然后在driver_manager.py里这样写from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service def get_chrome_driver(): service Service(ChromeDriverManager().install()) options webdriver.ChromeOptions() options.add_argument(--start-maximized) return webdriver.Chrome(serviceservice, optionsoptions)这么处理之后新同事拉到代码环境第一次执行用例就会自动完成驱动适配不再因为环境问题卡住半天。框架的“开箱即用”程度直接决定了团队能不能真正跑起来。4.2 无头模式与可视化模式如何动态切换本地调试用例时我们通常希望浏览器窗口弹出来方便观察执行过程。但在持续集成环境比如Linux服务器上跑定时任务没有图形界面浏览器必须用无头模式运行。这个需求不复杂但很多初学者会把两种情况分开写两套代码。正确的做法是读取配置def create_browser(browser_typechrome, headlessFalse): if browser_type chrome: options webdriver.ChromeOptions() if headless: options.add_argument(--headlessnew) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) service Service(ChromeDriverManager().install()) return webdriver.Chrome(serviceservice, optionsoptions)配置项放在setting.py里HEADLESS False # 本地调试时 FalseCI 环境置为 True BROWSER_TYPE chrome一个小细节Linux环境中跑无头模式必须加--no-sandbox和--disable-dev-shm-usage否则Chrome启动时会报错。这个坑几乎每个从Windows搬到Linux的人都踩过。注意--headlessnew是Chrome新版无头模式参数旧版本的Chrome只支持--headless。为了兼容性建议统一使用新版Chrome并配合webdriver-manager保持驱动版本一致。5. 等待策略与稳定定位的实用细节5.1 睡眠等待为什么是测试脚本的“万恶之源”我见过很多刚入行的测试同学写自动化定位不到元素就加time.sleep(3)然后用例能跑了就提交了。这种做法短期有效后患无穷。time.sleep是硬性等待不管页面元素是否已经就绪程序都会睡够指定的时间。如果网络快页面2秒就加载完了脚本还要白等1秒如果网络慢页面5秒才加载完成sleep(3)就会直接报错。也就是说用sleep既降低执行效率又制造不稳定因素。Selenium提供的WebDriverWait是智能等待它会在指定时间内反复尝试某个条件一旦条件满足立即继续执行时间耗尽才抛出异常。这才是UI自动化等待的正确姿势。element WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, submit)) )这段代码的含义是在10秒内每0.5秒检查一次“submit按钮是否可点击”可点击就立即返回不可点击就继续等直到超时。5.2 显式等待的常用预期条件在实际项目中我常用以下几类预期条件presence_of_element_located元素已出现在DOM中不要求可见visibility_of_element_located元素在DOM中且可见element_to_be_clickable元素可见且可点击text_to_be_present_in_element元素文本包含指定内容frame_to_be_available_and_switch_to_itiframe加载完成且可切入选择哪个预期条件取决于后续要做的操作。如果只是读取文本用visibility_of_element_located就行如果要点击必须用element_to_be_clickable因为“可见”和“可点击”在极端情况下不是同一回事。5.3 定位不到元素的典型排查路径脚本稳定运行一段时间后突然开始批量失败90%以上是定位问题。定位不到的排查顺序建议是这样第一步打开浏览器开发者工具在Elements面板中确认元素是否真实存在。有时候你以为是页面上的元素实际上它在iframe里或者根本就是动态渲染的内容。第二步如果存在确认当前DOM中是否有多个匹配项。Selenium的find_element默认返回第一个这个“第一个”不一定是你想要的。第三步检查元素的属性是否是动态的。现在前端框架React、Vue生成的元素ID经常带有动态后缀比如btn_1698326400这类定位必须改用相对稳定的属性比如name、>import pytest from utils.driver_manager import create_browser pytest.fixture(scopesession) def driver(): 整个测试会话只启动一次浏览器 browser create_browser() yield browser browser.quit() pytest.fixture() def login_page(driver): 每个用例都通过登录页进入系统 from pages.login_page import LoginPage page LoginPage(driver) page.load() return page这里有一个值得注意的设计决策driver的scope是session意味着整个测试执行期间浏览器只开关一次用例之间共享同一个浏览器进程。这种方案的优点是执行效率高缺点是用例之间容易产生状态污染——上一个用例没有退出登录下一个用例就会受影响。如何取舍我的经验是回归测试建议每个用例独立开启会话虽然慢一点但足够稳。冒烟测试或者定时巡检可以使用session级浏览器因为用例数量少且执行顺序可控。如果你刚开始搭框架我建议先写function级别的fixturepytest.fixture() def driver(): browser create_browser() yield browser browser.quit()这种写法牺牲了速度换来了用例之间的绝对隔离。等用例规模上来、稳定性验证OK了再尝试优化成session级。6.2 用例失败自动重跑治标不治本但很有用UI自动化天然存在偶发性失败网络抖动、服务器响应慢、页面渲染延迟都会让本来通过的用例莫名失败。这种偶发问题如果每次都要人工确认消耗的精力比写用例还大。pytest-rerunfailures插件可以给用例增加失败重跑机制。在pytest.ini中配置[pytest] addopts -v -s --htmlreports/html/report.html --self-contained-html reruns 2 reruns_delay 2reruns 2表示失败后重跑2次reruns_delay 2表示每次重跑间隔2秒。这样设置后单次偶发失败不会立刻导致整条用例标红如果重试后仍然失败则确认是真实问题。但必须强调一点重跑机制只是降低噪音不是掩盖问题的手段。如果某条用例频繁进入重跑流程说明它本身就不稳定需要从代码层面排查根因而不是一味依赖重试。6.3 参数化实现数据驱动的进阶用法WebUI自动化的数据驱动本质上是把测试数据与用例逻辑分离。Pytest的parametrize装饰器让这个过程异常简洁import pytest pytest.mark.parametrize(username,password,expected, [ (admin, 123456, 登录成功), (admin, wrong_pwd, 用户名或密码错误), (, , 请输入用户名), ]) def test_login(driver, username, password, expected): login_page LoginPage(driver) login_page.load() login_page.login(username, password) assert expected in login_page.get_page_text()这样做的好处是新增测试数据不需要修改用例代码只需要在参数列表里加一行。数据量大了以后还可以把数据抽到外部文件运行时读取。我在实际项目中常常混合两种方式比较固定的边界数据用parametrize写在代码里按业务场景组合、需要频繁更新的数据放在YAML或Excel文件中。后者的可维护性更高但实现复杂度也高一些需要自己写一个读取外部数据并转换成Pytest参数的函数。7. 断言、日志与失败证据让报错信息可读7.1 一个完整的日志模块该记录什么很多自动化用例失败后测试人员第一反应是看报告但报告里只有“断言失败”这一句话完全不知道页面当时发生了什么。这时候日志的价值就体现出来了。我封装的日志模块大致长这样import logging import os from datetime import datetime def setup_logger(): log_dir logs if not os.path.exists(log_dir): os.makedirs(log_dir) log_file os.path.join(log_dir, ftest_{datetime.now().strftime(%Y%m%d_%H%M%S)}.log) logger logging.getLogger(webui_auto) logger.setLevel(logging.INFO) formatter logging.Formatter(%(asctime)s - %(levelname)s - %(message)s) file_handler logging.FileHandler(log_file, encodingutf-8) file_handler.setFormatter(formatter) logger.addHandler(file_handler) return logger日志里我建议记录四类信息用例开始执行标记明确的步骤名称关键操作比如“输入用户名admin”“点击登录按钮”断言结果成功或失败的具体信息异常堆栈和错误截图路径有了这几类日志出问题后查看日志文件就能还原用例执行现场定位是哪一步出的错。7.2 失败自动截图比日志更直观的证据有些问题靠文字描述不清楚比如页面布局错乱、元素被遮挡、弹窗遮挡了内容。这时候一张截图胜过千言万语。在conftest.py中添加一个失败截图的钩子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: screenshot_dir reports/screenshots os.makedirs(screenshot_dir, exist_okTrue) file_name f{item.name}_{datetime.now().strftime(%Y%m%d_%H%M%S)}.png file_path os.path.join(screenshot_dir, file_name) driver.save_screenshot(file_path) report.screenshot file_pathPytest的钩子机制允许我们在用例执行结束后拿到报告对象如果报告状态是failed就自动截图并保存到指定目录。这个功能几乎零成本但对日常排障的帮助非常大。注意截图最好在用例真正失败后立即执行晚了的话页面状态可能已经被下一个用例的启动逻辑改变截图就失去意义了。8. 报告集成与持续集成流水线配置8.1 pytest-html报告的美化与信息补全pytest-html生成的基础报告比较朴素默认只展示用例名和执行状态。我会额外配置一个conftest.py中的钩子给报告添加环境信息def pytest_html_report_title(report): report.title WebUI自动化测试报告 pytest.hookimpl def pytest_configure(config): config._metadata[项目名称] 某Web管理系统 config._metadata[测试环境] Test_env config._metadata[浏览器] Chrome报告中extra信息也可以通过钩子添加比如把失败的截图以base64内嵌到HTML报告中import base64 pytest.hookimpl(tryfirstTrue) def pytest_runtest_makereport(item, call): ... if report.failed: with open(file_path, rb) as f: base64_image base64.b64encode(f.read()).decode() html fimg srcdata:image/png;base64,{base64_image} stylewidth:300px/ report.extra [pytest_html.extras.html(html)]这样一来打开的HTML报告里直接能看失败截图不需要去文件目录里找图片。对于交付报告给非技术同事看的情况这个细节体验好很多。8.2 把自动化任务挂进持续集成从手动执行到定时巡检框架在本地跑通只是第一步真正体现价值的是放到持续集成流水线里定时执行。通用做法是使用Jenkins或者GitLab CI。Jenkins中配置一个自由风格的项目增加一个“执行Shell”的构建步骤cd /data/webui_auto_framework python -m venv venv source venv/bin/activate pip install -r requirements.txt -q pytest testcases/ --htmlreports/html/report.html定时执行可以用cron表达式比如每天凌晨2点跑全量回归0 2 * * *执行完成后配置“发布HTML报告”的插件让团队成员直接通过浏览器查看结果。如果任务失败还可以配置企业微信或钉钉机器人推送通知。在配置CI的过程中我遇到最多的问题是环境变量和路径问题。本地执行通过的用例搬到Linux服务器上通常会因为以下原因失败服务器上没有安装系统依赖库比如Chrome浏览器缺失中文路径或编码问题导致截图文件名异常无头模式下窗口大小未设置导致某些元素不可见解决这些问题的通用思路是先在服务器上手动跑一遍把所有失败原因逐个击破再挂到定时任务里。9. 框架落地过程中的常见问题排查速查表下面这个表是我在多个团队辅导过程中整理的高频问题列表直接按表排查能省很多时间。问题现象可能原因排查与解决方案启动浏览器报“session not created”驱动版本与浏览器版本不匹配使用webdriver-manager自动匹配驱动手动确认chromedriver --version与Chrome版本一致元素定位报NoSuchElementException页面未加载完成或定位表达式错误检查定位表达式改用显式等待确认是否在iframe中在Elements面板确认元素状态点击元素报ElementClickIntercepted元素被其他元素遮挡用JS执行点击driver.execute_script(arguments[0].click();, element)或者先关闭遮罩弹窗用例偶发失败、重试后通过网络波动或服务器响应慢确认是否属于环境问题适当增加超时时间分析是否存在严重依赖顺序的用例同一元素在不同浏览器下定位不一致前端框架生成动态属性优先使用稳定的自定义属性data-testid避免使用CSS类名做定位或使用XPath文本定位首次执行慢后续执行也慢浏览器启动配置了太多加载项删除不必要的浏览器插件和扩展禁用图片加载考虑复用同一个浏览器实例截图全是黑屏或空白浏览器窗口未激活或页面未截获在无头模式下确保设置--window-size1920,1080必要时使用driver.fullscreen_window()其实不少问题在本地调试中根本发现不了只有用例在无人值守的环境里批量执行时才会暴露。所以我的习惯是新用例在本地跑通了不算完至少要放到CI环境里连续跑三天观察稳定性之后再决定是否纳入回归集。10. 从零搭建之后框架还能往哪个方向扩展框架搭建完成、基础用例稳定运行之后很多团队第一反应是堆用例。但我建议先停下来思考一下扩展性。第一个方向是接口测试与UI测试的联动。同一套框架里先跑接口用例验证服务端逻辑再跑UI用例验证关键链路数据准备完全复用接口的返回值。这样既避免了UI操作准备数据的低效也提升了整个自动化体系的价值密度。第二个方向是测试数据的统一管理。当用例规模达到几百条后YAML文件散落在各处管理和检索都会成为负担。可以考虑引入一个简单的外挂管理平台来维护测试数据前端页面配置、后端存储、用例运行时按需读取这也是从“框架”走向“平台”的关键一步。第三个方向是视觉回归。UI自动化除了验证功能存在还能验证样式是否在可接受范围内。在原有框架上增加截图比对能力每次版本迭代后自动化截取页面关键区域的图片与基线图做像素级比对能发现那些功能逻辑正常但视觉效果被破坏的隐性回归。第四个方向是大模型辅助用例生成。近几年大语言模型的能力越来越强已经有不少方案可以基于页面DOM结构和用户行为描述直接生成Selenium脚本。框架如果保持页面对象层、用例层充分解耦这类工具生成的脚本可以低成本接入现有体系。目前已有纯开源方案支持“页面截图自然语言→自动生成PO代码与用例代码”的工作流虽然还没到完全可靠的程度但方向是确定的。最后再从个人经验角度聊点实在的。我见过不少框架目录结构很漂亮技术栈也很新但团队根本用不起来。原因不是技术不行而是维护成本被低估了。UI自动化的维护成本大头一定在用例层——页面一改PO要跟着更新产品逻辑一改用例断言要跟着调整。所以框架搭建的过程中一定要把“降低维护成本”作为最高优先级的设计原则。尽量少写重复代码尽量多复用公共方法元素定位尽量集中在PO层用例之间尽量不产生依赖。做到这几条框架的寿命才会足够长团队才不会一边写自动化一边骂自动化。这套框架本身的代码量并不大核心模块加起来千把行却能把UI自动化从一个“脚本集合”提升到一个“可持续交付的工程体系”。如果在搭建过程中你发现用例跑得越来越不稳先别急着加耗时处理或者强硬等待回过头看看结构上是不是有哪些地方没有收敛好——绝大多数“不稳定”的根源都不是技术问题而是设计问题。

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

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

免费获取报价 →
↑