资讯详情

资讯详情

Python+Selenium UI自动化测试框架搭建:分层设计、等待机制与用例实践

做UI自动化测试差不多五年Python和Selenium一直是我最常用的组合。身边常有测试同行问我一套能真正跑起来、能进团队的自动化测试框架到底应该怎么搭很多人学了一堆教程最后还是卡在元素定位、等待机制和用例写法上。今天这篇就围绕PythonSelenium这套技术栈从设计思路到手写代码完整拆一个实用框架。这套框架能做什么它能帮你把重复的回归测试从手工点击变成自动执行把测试用例组织成清晰的分层结构失败时自动截图、生成报告跑完大概十分钟就能知道所有功能是否正常。适合刚开始接触自动化测试的测试工程师也适合想重构旧脚本体系的开发测试人员。下面不聊虚的直接讲设计、结构和我能跑起来的Demo。1. 框架整体设计与思路拆解1.1 为什么是PythonSelenium而不是其它组合选择PythonSelenium很大程度是团队现实决定的。Python语法简单测试人员不需要花三个月去熟悉语法Selenium对应WebDriver标准协议能驱动Chrome、Firefox、Edge等主流浏览器网上的问题和案例几乎搜一个有一个。相比之下老牌QTP现在叫UFT录制回放上手快但脚本结构和维护成本真的一言难尽Robot Framework关键字驱动很灵活可封装的复杂度也不低带Node背景的团队可能选WebdriverIO但它的资料和插件生态没有Python这边好养活。我个人Build这套组合的逻辑很简单用最少的学习成本换取最高的可维护性。Python生态里有Pytest做测试执行、Allure或Pytest-HTML出报告、PyYAML管配置、Requests管接口这些跟Selenium都能无缝衔接。真正复杂的是被测系统本身而不是测试工具工具上能少折腾就少折腾。1.2 框架分层让代码不再是一盘散沙很多新手写的自动化脚本问题不在于代码跑不起来而在于所有逻辑混在一个文件里元素定位写在函数里测试步骤写在用例里测试数据硬编码在脚本里。第一版能跑到了第二个版本就没人敢动了。我常用的是四层结构用例层、页面对象层、驱动层、配置数据层。打个比方用例层是顾客点单页面对象层是后厨做菜驱动层是采购食材配置数据层是菜单和配料表。顾客不需要知道后厨怎么炒后厨也不需要直接跟顾客解释。替换其中一层其他层不会受到牵连。层级职责对应代码位置用例层描述业务场景做断言test_cases/页面对象层封装元素定位和操作一个页面一个类pages/驱动层负责创建浏览器实例处理浏览器选项conftest.py配置数据层管理URL、账号、超时时间、数据文件config/、data/页面改动时只需要改页面对象层用例层不用动测试数据变了改配置文件和数据文件就行。这套模式的本质是控制变化点因为UI自动化测试里变化最频繁的就是元素属性和页面流程。1.3 稳定性比花哨功能更重要自动化测试最让人头疼的不是写不出用例而是今天能过、明天就挂。所以框架设计里稳定性必须排在所有功能前面。我每次搭框架都强制内置三样东西显式等待封装、统一日志、失败截图。等待不到位用例会随机失败日志不统一出了问题很难定位是在哪一步挂的失败没有截图报错信息里只有一堆HTML调试全靠猜。这三样东西在后面的章节会逐个演示。框架不需要为了炫技去引入一堆组件能用最简单的代码解决稳定性问题才是长期最省事的方案。2. 环境搭建与工具选型2.1 安装Python、Selenium和浏览器驱动环境这块新手比较容易卡在浏览器驱动上。先唠叨一下版本关系Selenium通过浏览器驱动比如ChromeDriver来指挥浏览器驱动版本必须和浏览器主版本匹配否则启动时直接报错。安装步骤按下面走从Python官网下载安装包记得勾选Add Python to PATH避免后面找不到命令。命令行验证python --version。安装依赖pip install selenium pytest pytest-html pyyaml。根据自己浏览器版本下载对应驱动把驱动放到能被PATH找到的目录例如Windows下放C:\WebDriver并配置环境变量。Selenium 4.x内置了Selenium Manager很多情况下会自动寻找驱动省去手动配置的时间但公司网络限制、浏览器版本太新时它不一定管用。所以我还是建议你掌握手动下载、手动配置驱动的方法遇到问题能更快定位。2.2 用Pytest做测试运行器而不是unittestPython自带的unittest不是不能用只是用它写自动化用例体验不好断言写法又长又碎setUp和tearDown做全局初始化很别扭各种条件跳过和参数化做起来也繁琐。Pytest在社区里几乎成了Python测试的事实标准几个核心优势直接解决了这些问题断言直接使用assert写起来自然。Fixture替代setup/teardown能精细控制实例的创建和销毁范围。大量插件比如失败重跑、报告生成、并发执行随用随加。下面是一个最基础的driver fixture放在test_cases/conftest.py里import pytest from selenium import webdriver from utils.config_reader import Config pytest.fixture(scopefunction) def driver(): browser Config.get(browser, defaultchrome) if browser chrome: options webdriver.ChromeOptions() options.add_argument(--window-size1920,1080) driver webdriver.Chrome(optionsoptions) else: driver getattr(webdriver, browser.capitalize())() driver.get(Config.get(base_url)) yield driver driver.quit()scopefunction表示每个测试用例独享一个浏览器实例用例之间互不污染。这是UI自动化稳定性的重要基础。2.3 项目目录结构规划目录结构不一定要很庞大但我建议一开始就按职责分好后面代码不迷路。我常用的目录骨架如下auto_test/ ├── config/ │ └── config.yaml ├── pages/ │ ├── base_page.py │ └── login_page.py ├── test_cases/ │ ├── conftest.py │ └── test_login.py ├── utils/ │ ├── config_reader.py │ ├── logger.py │ └── screenshot.py ├── data/ │ └── login_data.json ├── logs/ └── reports/每个目录的定位config存放运行环境、浏览器的参数pages放页面对象test_cases放测试用例自然也可以理解为业务场景和断言utils放公共工具比如读取配置、写日志、截图data存放测试数据账号、用户名这类内容不要直接写死在脚本里logs和reports是运行产物。这样划分以后新人接手也能快速找到自己要改的地方。3. 核心细节解析与实操要点3.1 元素定位的细节不要只会背XPath元素定位是UI自动化里最碰运气的一环。网上的教程往往只教XPath语法却很少教你怎么选择稳定定位方式。我自己的优先级是ID 唯一属性 CSS Selector XPath。ID稳定就优先用ID没有ID就找name、>username_input driver.find_element(By.CSS_SELECTOR, input[nameusername]) password_input driver.find_element(By.CSS_SELECTOR, input[namepassword]) login_button driver.find_element(By.CSS_SELECTOR, button.login-btn)XPath也有必要掌握尤其是处理复杂页面时# 不要这样写页面结构一变就废 element driver.find_element(By.XPATH, /html/body/div[1]/div[3]/form/input) # 尽量基于稳定属性定位 element driver.find_element(By.XPATH, //div[classlogin-box]//input[nameusername])我踩过一个真实的坑某个前端框架生成的按钮ID每次刷新都会变像btn_ab3f89这种动态字符串脚本经常找不到元素。后来跟开发约定给核心按钮加了个>from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By def wait_clickable(driver, locator, timeout10): return WebDriverWait(driver, timeout).until( EC.element_to_be_clickable(locator) )用显式等待时默认超时我习惯给10秒如果某个页面特别慢单独给这个页面对象传更大的timeout。这样比统一的sleep(5)省时间也稳定得多。3.3 Page Object模式与BasePage封装Page Object模式的核心思想是一个页面对应一个类类里封装页面上的元素和对这些元素的操作测试用例只调用这些操作方法和断言结果。代码看起来会像这样class BasePage: def __init__(self, driver): self.driver driver def find_element(self, locator, timeout10): return WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located(locator) ) def click(self, locator): self.find_element(locator).click() def input_text(self, locator, text): element self.find_element(locator) element.clear() element.send_keys(text) def get_text(self, locator): return self.find_element(locator).text登录页继承这个基类from selenium.webdriver.common.by import By from pages.base_page import BasePage class LoginPage(BasePage): username_loc (By.CSS_SELECTOR, input[nameusername]) password_loc (By.CSS_SELECTOR, input[namepassword]) login_btn_loc (By.CSS_SELECTOR, button.login-btn) success_tip_loc (By.CSS_SELECTOR, div.welcome-msg) def login(self, username, password): self.input_text(self.username_loc, username) self.input_text(self.password_loc, password) self.click(self.login_btn_loc) def is_login_success(self): return self.find_element(self.success_tip_loc).text.startswith(欢迎)这样测试用例的逻辑就变得非常简单也更容易让人理解到底在测什么def test_login_success(driver): page LoginPage(driver) page.login(tester, 123456) assert page.is_login_success()4. 实操过程与核心环节实现4.1 从零搭建框架的完整步骤这一节我按自己真实的操作顺序走一遍你跟着做就能跑起来。第一步创建项目目录和虚拟环境mkdir auto_test cd auto_test python -m venv venv在Windows的CMD里面激活venv\Scripts\activate在macOS/Linux是source venv/bin/activate。虚拟环境能隔离不同项目的Python依赖防止全局包互相污染强烈建议养成习惯。第二步安装依赖pip install selenium pytest pytest-html pyyaml如果准备做失败重试再加pytest-rerunfailures。第三步创建utils/config_reader.py读取YAML配置import yaml class Config: _data None classmethod def load(cls, pathconfig/config.yaml): with open(path, encodingutf-8) as f: cls._data yaml.safe_load(f) classmethod def get(cls, key, defaultNone): if cls._data is None: cls.load() return cls._data.get(key, default)config/config.yaml里面放base_url: http://127.0.0.1:8080 browser: chrome timeout: 10第四步写utils/logger.py用Python标准库logging输出统一时间、级别和消息。这里可以不展开到很复杂但日志一定要有排查问题的时候全屏输出会让自己疯掉。第五步实现pages/base_page.py把元素查找、点击、输入、截图这些通用操作封装好。第六步实现具体页面比如pages/login_page.py。第七步在test_cases/conftest.py放driver fixture负责启动和回收浏览器。第八步编写一条能通过的用例运行pytest -s看到绿色通过后再逐步加页面、加组件。这套步骤的核心是“小步快跑”先让一个流程跑通再去堆框架组件。我见过很多团队第一周就在写通用框架结果跑了三周连一条完整用例都没有这是本末倒置。4.2 以登录模块为例写一套能直接用的用例我们继续完善登录模块的例子。为了让用例不依赖固定账号可以把测试数据放到data/login_data.json{ valid_user: { username: tester, password: 123456 }, invalid_user: { username: tester, password: wrong_password } }用例层用参数化读取数据import pytest import json from pages.login_page import LoginPage with open(data/login_data.json, encodingutf-8) as f: login_data json.load(f) pytest.mark.parametrize(user, [ login_data[valid_user], login_data[invalid_user] ]) def test_login(user, driver): page LoginPage(driver) page.login(user[username], user[password]) if user[username] tester and user[password] 123456: assert page.is_login_success() else: assert page.is_login_fail()为什么要把数据放到JSON而不是直接写在用例里因为用例需要覆盖多组正常、异常输入后期维护时直接改JSON文件不用动代码。数据驱动的本质是代码逻辑不变变化的数据和预期结果分离。4.3 失败截图和HTML报告给自动化装上仪表盘用例报错后光看一行AssertionError是不够的必须能看到页面当时的样子。我在conftest.py里用Pytest的hook实现失败自动截图import os import pytest pytest.hookimpl(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: report_dir reports os.makedirs(report_dir, exist_okTrue) driver.save_screenshot(os.path.join(report_dir, f{item.name}.png))这个hook会在每个测试用例执行结束时被调用只关心call阶段失败的情况拿到driver实例后截图到reports目录。这里的item.funcargs就是fixture返回的对象字典前提是fixture名字叫driver。报告可以继续用Pytest-HTML生成pytest --htmlreports/report.html --self-contained-html或者接Allure把测试步骤、截图、关键日志都聚合到一个漂亮的面板。团队成员看报告比翻控制台输出直观得多。5. 常见问题与排查技巧实录5.1 元素定位失败先排查别急着改XPath定位不到元素报NoSuchElementException的时候我的排查顺序是手动打开页面按F12检查元素是否存在。如果手动能看到但脚本找不到大多是把元素放进了iframe。如果元素在iframe里先切换到driver.switch_to.frame(frame_id)再定位。检查路径是否包含了动态值比如元素id每次刷新都变。检查页面是不是异步加载页面虽然出来了但元素还没渲染需要增加显式等待。我之前遇到一个经典的异步问题登录按钮是在某个接口返回之后才渲染出来的用find_element一上来就点必然报错。改成WebDriverWait等待按钮可点击后问题马上消失。所以在改定位器之前先确认是不是等待问题会更有针对性。5.2 浏览器驱动版本不匹配怎么快速解决启动浏览器时报SessionNotCreatedException: This version of ChromeDriver only supports Chrome version xxx基本就是驱动和浏览器主版本对不上。Chrome自动更新后很多机器第二天就跑不了了。处理步骤chrome --version # 确认浏览器版本然后去ChromeDriver仓库下载与浏览器主版本一致的驱动替换旧驱动。如果你是Selenium 4可以删除手动配置的driver路径让Selenium Manager自动匹配试试。但要注意公司网络如果有限制自动下载可能失败手动方案永远是最保底的。5.3 用例之间互相影响让执行顺序再也不背锅当我发现测试用例“单个执行全通过全量执行随机挂”八成是用例之间出现了状态污染。比如在一个用例里登录了好几个账号cookie和localStorage被反复覆盖或者前一个用例改了数据库里某条记录后一个用例刚好依赖这条记录。解决思路是让用例尽量独立pytest.fixture(scopefunction) def driver(): # 每个用例都开新浏览器彻底隔离状态 ...如果确实需要共享登录状态可以保留一个session级别的fixture但内部要做好cookie清理和失败恢复。我通常更推荐用例独立执行——UI自动化的慢是可以接受的但乱是不能接受的。5.4 测试跑不稳定试试失败重试和更合理的超时即使等待都写了用例偶尔还是会因为网络抖动、第三方服务慢而失败。这就是“Flaky Test”。可以在Pytest里加入失败重试pytest --reruns 2 --reruns-delay 1这个插件会让失败的用例重跑最多2次如果重试后通过报告里标记为RERUN至少能把偶发问题排查范围缩小。但重试不是万能的不能因为用例不稳定就一律重试5次那会掩盖真实Bug。我一般设置重试1-2次同时把显式等待超时控制在合理范围。还有一个细节不要在断言前用长长的time.sleep(30)等结果。更稳的做法是轮询接口或页面元素状态一旦符合预期就立刻继续。这套框架我从最原始的函数式脚本改到现在的分层版本中间踩过的坑远比写出来的多。最深的体会是自动化测试不是为了炫耀代码而是为了节省回归时间。框架没有绝对标准团队习惯不同、被测系统的复杂度不同允许灵活调整。如果你正打算搭自己的框架建议从小用例开始先让一条流程跑起来再逐步加页面、加报表、加重试一步步迭代。最后留个建议把日志和截图当成一等公民它们是你排查问题最可靠的眼睛。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →