简介:Python通用UI自动化测试框架2.0是一套面向软件/Web应用的UI自动化测试源码,帮助测试工程师和开发人员降低脚本编写与维护成本,快速构建可复用的自动化测试体系,解决手工回归耗时长、脚本重复率高的问题。资源包共768个文件,以299个Python脚本为主,同时包含示例sample、可执行exe、配置文件cfg/config、批处理bat、日志及说明文档等,整体约4.23MB,目录结构按功能模块划分,便于检索不同用途的源码。目前已有1749人学习下载,适合中高级测试人员参考,亦可作为团队内部搭建UI自动化基础设施的蓝本。框架对元素操作、检查点验证和异常回退策略进行了抽象,可结合Selenium、unittest/pytest及Page Object模式组织用例,2.0版本进一步优化了扩展性和易用性,既可作为项目直接落地的框架基础,也是学习UI自动化测试架构设计的不错素材。 做UI自动化这件事,喊口号的人多,真正把框架落地并维护到2.0的人少。我这边从1.x一路重构过来,踩过的坑比写过的用例还多。这次开源的这套Python通用UI自动化测试框架2.0源码,不是把Selenium和Pytest拼在一起交差,而是把页面对象管理、定位策略、等待机制、数据驱动、报告输出这几个模块全部解耦,能让一个刚接触自动化的新手照着文档把用例跑起来,也能让一个维护了几千条用例的团队在改版时不用抓狂。
这篇不只讲框架怎么用,更多是把设计思路和为什么这样做讲透。你拿到源码之后,不光能跑,还能知道哪块该改、怎么改,这才是2.0比1.x值钱的地方。
1. 整体设计与模块拆解
1.1 从1.x到2.0:这次重构解决的核心痛点
1.x版本最大的问题,是把页面元素定位字符串直接散落在用例代码里。当时图省事,driver.find_element(By.ID, "username")到处写,一个登录用例动辄一两百行。后面项目迭代,前端把按钮从id="loginBtn"改成>python run_all.py --env test --browser chrome
配置加载的逻辑不复杂,就是先读默认配置,再读指定环境的配置做覆盖合并。这个设计的好处是,同一个框架在不同环境跑,只需要改配置,不需要动代码。从零搭框架时,这一步一定不要跳过,不然后面多环境跑用例的时候会头疼。
2. 核心模块实现细节
2.1 Page Object基类设计:把重复动作收敛到一处
base_page.py是整个框架的地基。我封装的基类包含初始化接收driver、统一的元素查找入口、常用的文本输入和点击操作、以及断言的辅助方法。设计的关键点是所有操作都要走统一的查找链路,而不是到处写driver.find_element。
# core/base_page.py 部分核心代码 class BasePage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait( driver, timeout=Config.TIMEOUT, poll_frequency=Config.POLL_FREQUENCY ) def find_element(self, locator): """统一的元素查找入口,集成自动滚动与日志记录""" self.logger.info(f"查找元素: {locator}") element = self.wait.until( EC.presence_of_element_located(locator) ) # 滚动到可视区域,避免被遮挡或需要手动滚动的场景 self.driver.execute_script( "arguments[0].scrollIntoView({block: 'center'});", element ) return element def input_text(self, locator, text): element = self.find_element(locator) element.clear() element.send_keys(text) self.logger.info(f"输入文本: {text}")统一入口在后期体现出的价值很大。比如某个迭代要求所有元素在操作前先判断可点击状态,我只需要在find_element方法里加一段element_to_be_clickable的等待,所有页面对象就都生效了。如果每个用例直接操作Selenium,这种全局改造就要改全量代码。
2.2 智能等待策略:告别sleep赌运气
等待是UI自动化稳定性的核心之一。2.0的等待策略做了两级处理:初始化浏览器时设置一个偏保守的隐式等待,页面操作过程中用显式等待兜底具体条件。
实际实现上,我对最常见的几种等待场景做了封装:
- 等待元素出现:
wait_element_visible(locator) - 等待元素可点击:
wait_element_clickable(locator) - 等待元素消失:
wait_element_invisible(locator) - 等待页面加载完成:通过
document.readyState判断
这里有一个容易被忽视的坑:Selenium的隐式等待和显式等待不要混用。两个等待机制叠加会导致一些场景下超时时间被意外拉长,比如隐式等待设置了10秒,显式等待设置了10秒,实际可能出现等待20秒的情况。框架里默认把隐式等待设为0,完全依赖显式等待来控制节奏。
另外,判断页面加载完成不能只看readyState,很多单页应用是异步渲染的,DOM加载完不代表元素可见。最稳妥的判断标准是目标元素本身的状态,这也解释了为什么2.0里的等待条件都绑定具体的业务元素而不是页面全局状态。
2.3 定位器策略与元素管理
元素定位方式统一用By的元组以类属性形式管理。这样页面对象读起来可读性好,出问题也好排查。
class LoginPage(BasePage): """登录页面对象""" username_input = (By.CSS_SELECTOR, "[data-testid='username-input']") password_input = (By.CSS_SELECTOR, "[data-testid='password-input']") login_button = (By.CSS_SELECTOR, "[data-testid='login-button']") error_toast = (By.CSS_SELECTOR, "[data-testid='error-toast']")在实际项目中,我倾向于优先使用>login: valid_user: username: "test_user" password: "Passw0rd!2024" expected: "登录成功" invalid_password: username: "test_user" password: "wrong_pwd" expected: "用户名或密码错误"
用例中通过参数化读取这些数据:
# cases/test_login.py class TestLogin: @pytest.mark.parametrize( "case_data", DataLoader.load_yaml("data/login_data.yaml")["login"].values(), ids=lambda d: d["expected"] ) def test_login(self, case_data, init_driver): login_page = LoginPage(init_driver) login_page.login(case_data["username"], case_data["password"]) assert login_page.get_toast_message() == case_data["expected"]数据驱动的价值在回归测试时体现得最明显。想增加一组异常数据,不需要改代码,在YAML里加一条记录就行。即使不懂Python的测试同学也可以独立维护测试数据参与进来。
3. 完整实操:从环境搭建到跑通首个用例
3.1 环境准备与依赖安装
框架基于Python 3.10+,依赖的核心库包括:
selenium>=4.24.0pytest>=8.0.0pytest-html(或allure-pytest)PyYAML>=6.0webdriver-manager(浏览器驱动自动管理)
安装依赖:
pip install -r requirements.txt这里推荐webdriver-manager,会自动下载对应浏览器版本的驱动,省去手动配置驱动路径的步骤。团队新人接入项目时体验友好很多,不会在环境准备阶段卡很久。
注意:安装时建议使用虚拟环境。之前遇到过依赖全局环境冲突导致Selenium版本被降级的情况,排查了半天才发现是其他项目覆盖了依赖版本。
3.2 写一个真实的登录用例
第一步,在core/browser.py初始化浏览器。这里用的是webdriver-manager管理ChromeDriver,并且把浏览器退出操作放到conftest.py的Fixture中管理。
# core/browser.py def create_driver(browser="chrome", headless=False): options = getattr(webdriver, browser.capitalize()).Options() if headless: options.add_argument("--headless=new") options.add_argument("--start-maximized") driver = getattr(webdriver, browser.capitalize())( options=options ) return driver第二步,定义登录页面对象。继承BasePage,页面动作比如打开登录页、输入用户名密码、点击登录、获取错误提示,都封装为独立方法。
第三步,写用例。用例中只需要做三件事:加载页面对象、执行业务动作、断言结果。
整个流程跑通之后,你会发现用例代码极其简洁,核心业务逻辑一目了然。写新用例的模式固定为:先建Page类写元素定位,再写用例做动作编排和断言。执行用例:
pytest cases/test_login.py -v --alluredir=reports/allure-resultsconftest.py里我写了一个init_driver的Fixture,带yield的形式保证用例结束后浏览器正常关闭,即使断言失败也不会残留僵尸进程。
3.3 报告、日志与失败截图
2.0在测试框架的基础上增加了失败自动截图机制。conftest.py里用pytest_runtest_makereport钩子,在用例失败时自动调用驱动截屏,并把图片路径附加到Allure报告中。
@pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: driver = item.funcargs.get("init_driver") if driver: screenshot_path = f"reports/screenshots/{item.name}_{int(time.time())}.png" driver.save_screenshot(screenshot_path) allure.attach.file( screenshot_path, name="失败截图", attachment_type=allure.attachment_type.PNG )日志系统用的是Python标准库logging,按天滚动生成文件,格式包含时间、模块名、级别、信息。刻意没引入loguru,是为了少一个依赖,也让新手更清楚日志系统是怎么工作的。
4. 常见问题排查与实战避坑
4.1 用例不稳定、偶发失败怎么办
UI自动化最让人头疼的问题就是用例在本地能过、在CI或回归环境偶发失败。这类问题九成是等待条件设置不合理。排查时我习惯看两个东西,一个是失败时间点,如果都发生在页面交互阶段,基本可以确定是元素还没就绪;另一个是看截图,元素被遮挡或者页面还是空白,要么是等待条件不对,要么是前端异步组件响应慢。
建议遵循这样的优先级来调整:
- 优先使用业务相关的显式等待,等待的始终是这个元素可用
- 不推荐加
time.sleep,哪怕真的需要等待一个固定时间,也应该等待固定时间之后出现的某个结果 - 如果元素可见但点击偶发失效,用
element_to_be_clickable而不是presence_of_element_located
提示:保持用例的独立性比任何等待策略都重要。用例之间如果存在执行顺序依赖,数据状态互相影响,再稳的等待也救不回来。2.0框架里我强制要求每条用例都能单独运行,这也是排查问题的基础。
4.2 Selenium版本升级后定位失效
做过一年多自动化的人大概率遇到过:浏览器自动升级后,之前能跑的用例突然大面积失败。这里不一定是定位器写错了,很可能是浏览器驱动和浏览器版本不匹配。用webdriver-manager之后这个问题基本消除,因为驱动是自动匹配的。
但版本升级后真正要注意的是Selenium 4的语法变化和等待机制调整。比如find_element_by_id这种旧写法已被移除,必须用find_element(By.ID, ...)。2.0源码本身跑在新版本上,同时把容易踩坑的地方都做了封装,只要不绕过BasePage直接操作驱动,升级就不会炸。
4.3 用例执行慢、难以并发
有些团队发现用例一多跑起来太慢,就想着并发执行。我没在一开始就上并发,而是先给用例按业务域做了标记,把不依赖公共数据的用例用pytest -n auto跑并发,有状态依赖的保留串行。
pytest-xdist并发执行最需要注意的坑是浏览器实例隔离和测试数据隔离。每个并发worker必须用独立的浏览器会话,测试数据也要保证互不覆盖。框架里运行并发时,每个worker实例都会从配置中取独立前缀的用户数据,避免撞车。
4.4 元素定位器维护的成本问题
这是我回访了几个使用团队的体验后重点做的优化。页面对象层把所有定位器集中起来,UI改版时只改对应Page里的元组,用例不需要动。2.0里我还加了定位器标准化检查脚本,扫描代码里是否直接出现了find_element调用,保障新代码不退回脚本型写法。
| 问题现象 | 可能原因 | 排查/解决方式 |
|---|---|---|
| 用例偶发找不到元素 | 等待条件过短或页面异步渲染 | 换成显式等待并绑定目标元素状态 |
| 点击报错提示元素被拦截 | 弹窗或浮层遮挡 | 先判断元素是否可点击,必要时滚动到可视区域 |
| 浏览器一更新就全部失败 | 驱动与浏览器版本不匹配 | 用webdriver-manager自动匹配驱动版本 |
| 元素定位到但取值不是最新 | 页面状态未刷新 | 等待文本/属性变化,用text_to_be_present_in_element |
| 并发跑用例相互影响 | 浏览器实例或测试数据共享 | 保证每worker独立浏览器会话和独立测试数据 |
5. 关于框架演进的一点体会
框架从1.x走到2.0,我最深的体会是:一套测试框架的价值不在于用了多新的技术,而在于它是否真正降低了团队的协作成本和维护成本。2.0版本里很多设计都是从“这里大家总改”,反推出来的。比如定位器集中管理,是因为一个前端改版改到怀疑人生;等待机制重新设计,是因为几十条用例在CI上随机红。如果你也在维护自己的测试框架,建议你也去翻一翻最近三个月变动的用例,那些总是改来改去的地方,就是框架需要重构的地方。
源码里附带了一份使用文档和完整的示例工程,照着README从环境搭建到跑通第一个用例,基本半小时内能完成。后续如果要扩展,比如接入CI、加钉钉通知、做更精细的失败重跑,框架都预留了扩展点,欢迎改完之后回来交作业。
本文还有配套的精品资源,点击获取