简介:这是一份面向自动化测试初学者与测试开发工程师的Python自动化测试框架设计源码包,围绕测试用例组织、流程控制、UI操作与数据管理搭建出清晰可扩展的项目骨架,适合用于学习或直接裁剪落地。压缩包共57个文件,包含36个Python源码、9张界面截图、5个说明文档、3个XML配置与2个CSV数据文件,整体仅251KB。源码中既有main入口、controller调度、UI层封装,也有lib依赖、data测试数据与testcases用例目录;XML和CSV用于配置与数据驱动,截图与文档则辅助理解运行结果。testcases按成员分组存放单元、集成与组合测试用例,案例层次分明;readme说明、screenshots目录与.gitignore等配套齐全,便于对照学习框架的调用关系、异常处理思路和工程管理方式。目前已有119人学习浏览。读者可获得一套完整的测试框架搭建思路,包括测试数据外置、命令行参数解析、UI自动化操作封装、测试流程调度与结果记录等实用写法,对后续编写和维护自动化脚本有直接帮助。
1. 从 35 个 Python 源码文件里拆出一套自动化测试框架的骨架
拿到这套源码时第一感觉是“这不像教学 demo,而像一个真实跑过的测试工程”。根目录下 controller.py、main.py、UI.py 三个核心文件明确分工,testcases 里按开发者缩写分目录,每个目录下又是 unit、integration、combinatorial 三类测试用例的固定组合,再加上 data、lib、screenshots、config 各自独立——这是一套标准的“入口 + 调度 + 页面操作层 + 用例层 + 数据层”的 Python 自动化测试框架结构。它的价值在于让你不用从零摸索框架分层,直接照着这套设计去理解 pytest 与 Selenium 如何协作、数据驱动怎么落地。对于想搭建可维护的 Python 自动化测试框架的团队,以及想从脚本堆里抽身做框架重构的测试开发,这份源码都值得逐文件过一遍。下面我按源码实际结构,从入口到用例逐层拆开讲。
2. 框架分层与核心调度:main.py 与 controller.py 各管哪一段
2.1 main.py 作为启动入口:参数解析决定运行范围
main.py 在多数 Python 自动化测试框架里承担的是“接受外部输入并转译成测试命令”的任务。这套源码里的 main.py 同样如此,常见做法是用 argparse 解析命令行参数,把“跑哪类用例、跑哪个模块、是否生成报告”这类信息提取出来,再交给下游执行。参考其设计,入口代码一般长这样:
import argparse import pytest def parse_args(): parser = argparse.ArgumentParser(description="自动化测试框架入口") parser.add_argument("--testcase", type=str, default="testcases", help="指定测试用例目录,默认收集 testcases 下所有用例") parser.add_argument("--level", type=str, choices=["unit", "integration", "combinatorial"], default="unit", help="按用例级别过滤:单元/集成/组合") parser.add_argument("--html", type=str, default="report.html", help="测试报告输出路径") return parser.parse_args() if __name__ == "__main__": args = parse_args() pytest.main([args.testcase, "-k", args.level, f"--html={args.html}"])这段代码的核心逻辑是把命令行参数映射成 pytest 的收集规则:--testcase控制扫描目录,--level通过-k表达式过滤用例名,--html指定报告文件。参数里比较关键的是--level,因为源码的用例文件命名里都带unit、integration、combinatorial标识,-k做关键字匹配正好能按级别筛选,不需要改任何 pytest 配置。实际使用中,如果你只想跑某个人的用例,也可以把--testcase指向具体子目录,比如python main.py --testcase testcases/hjw。
2.2 controller.py 的调度职责:收集用例与处理执行顺序
controller.py 名字带 “controller”,在框架里的角色是执行控制器,负责在 pytest 真正接管之前做一次“预编排”。比如检查被测系统是否就绪、初始化日志目录、清理上次的截图残留,再把收集到的用例按依赖关系排序后交给执行器。参考这套框架的目录结构,controller 里一般包含三类方法:
import os import logging from datetime import datetime class TestController: def __init__(self, root_dir): self.root_dir = root_dir self.log_dir = os.path.join(root_dir, "logs") os.makedirs(self.log_dir, exist_ok=True) def precheck(self): # 检查关键依赖:浏览器驱动、被测系统地址、数据文件是否齐全 assert os.path.exists(os.path.join(self.root_dir, "data")), "data 目录缺失" def collect(self, keyword): # 按文件名关键字收集用例,返回用例文件路径列表 cases = [] for dirpath, dirnames, filenames in os.walk(os.path.join(self.root_dir, "testcases")): for name in filenames: if name.endswith(".py") and name.startswith("test_") and keyword in name: cases.append(os.path.join(dirpath, name)) return sorted(cases) def run(self, keyword): self.precheck() cases = self.collect(keyword) logging.info(f"共收集 {len(cases)} 个用例文件,开始执行") # 这里可以按需调整执行顺序,比如集成测试排在单元测试之后这里值得注意的一点是:很多初学者会把调度逻辑全部塞进 pytest fixture 里,导致 conftest.py 臃肿不堪。而 controller 独立出来的好处是,你在不引入 pytest 的情况下也能单独测试用例收集逻辑。collect方法里的sorted()很关键,它让执行顺序是稳定可预期的——这对 UI 自动化测试尤其重要,因为页面级用例对执行顺序的敏感度远高于纯逻辑单元测试。
2.3 data 与 config 目录:测试数据与配置的分离设计
源码里有data和config两个独立目录,这是框架设计里“数据不落地”原则的体现。data下存放 CSV 格式的测试数据,config下存放 XML 格式的配置项。这样区分的意义在于:CSV 适合描述“多组输入输出对”,天然适配参数化测试;XML 适合描述“键值型配置”,比如环境地址、超时时间、浏览器类型。读取 CSV 做参数化的常见写法是:
import csv import pytest def load_test_data(csv_path): cases = [] with open(csv_path, "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: cases.append((row["username"], row["password"], row["expected"])) return cases @pytest.mark.parametrize("username,password,expected", load_test_data("data/login_data.csv")) def test_login(username, password, expected): # 逐行执行登录用例 assert expected in perform_login(username, password)CSV 文件的列名要和parametrize的参数名严格对应,这是这条链路上最容易出错的点。如果 CSV 里某一行的期望结果包含特殊字符,建议在断言时用in而不是==,避免编码或隐形空格导致用例误报。lib目录则存放被多个模块引用的公共封装,与业务用例解耦,这样底层库更换时测试用例文件不需要大改。
| 目录名 | 存放内容 | 变更频率 | 是否纳入版本控制 |
|---|---|---|---|
| data | 登录数据、下单数据等 CSV 文件 | 高 | 是 |
| config | 环境地址、超时时间等 XML 配置 | 低 | 是 |
| lib | Selenium 驱动封装、断言封装 | 低 | 是 |
| screenshots | 运行时生成的截图 PNG | 高 | 否(通常 gitignore) |
| logs | 运行日志 | 高 | 否 |
这个表格的意义是让团队一眼看清哪些东西需要频繁改动、哪些属于基础设施。实际项目里 data 和 screenshots 最容易在版本控制上产生冲突,建议在.gitignore里显式忽略screenshots/和logs/,而data/保留提交,因为它是测试用例的输入依据,丢了数据用例就无法复现。
3. UI 自动化层:UI.py、图标资源与截图证据链的设计
3.1 UI.py 里的页面操作封装:定位、等待与动作分离
自动化测试框架设计里最核心的封装就是 UI 操作层。UI.py 在这套源码里承担的就是“页面对象”的职责——把页面上元素定位和操作逻辑从测试用例里抽离出来。以登录页面为例,典型的 UI.py 内部结构是:
from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class LoginPage: def __init__(self, driver): self.driver = driver # 元素定位集中在属性区,便于统一修改 self.username_input = (By.ID, "username") self.password_input = (By.ID, "password") self.login_button = (By.XPATH, "//button[contains(text(), '登录')]") def login(self, username, password): # 显式等待替代强制 sleep,降低用例不稳定概率 WebDriverWait(self.driver, 10).until( EC.presence_of_element_located(self.username_input) ).send_keys(username) self.driver.find_element(*self.password_input).send_keys(password) self.driver.find_element(*self.login_button).click()在 Selenium 自动化测试框架中,元素定位符集中定义在页面类的顶部属性区,而不是散落在测试用例里,这是 page object 模式的基本要求。WebDriverWait配合expected_conditions是比time.sleep()可靠得多的等待方式——前者在元素提前出现时会立即继续执行,后者无论如何都要等满固定时间。需要注意find_element(*self.username_input)中的星号是解包操作,把(By.ID, "username")这个元组展开成两个位置参数,新手漏写星号是常见报错点。
3.2 screenshots 与 screen_shot.py:失败场景的证据管理
源码里screenshots文件夹和screen_shot.py并存,说明这套框架已经考虑到 UI 测试的核心痛点——用例失败时只有报错信息不够,需要截图作为现场证据。截图模块的设计重点不在“怎么截”,而在“何时截、存哪里、怎么命名”。参考其思路,实现一般是:
import os from datetime import datetime def take_screenshot(driver, tag="failure"): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"{timestamp}_{tag}.png" filepath = os.path.join("screenshots", filename) driver.save_screenshot(filepath) return filepath def capture_on_failure(driver): """在断言失败时附加截图,并打印保存路径方便定位""" path = take_screenshot(driver, tag="failure") print(f"[截图已保存] {os.path.abspath(path)}")截图文件名的设计比为固定名覆盖存储重要得多。带时间戳的文件名保证每次失败都留痕,不会被下一次覆盖;tag参数则可以区分断言失败还是异常崩溃。实际运行中,如果同一轮测试出现多个失败,建议在文件名里再拼上用例名,否则只能靠时间戳回溯,排查效率会打折扣:
filename = f"{timestamp}_{testcase_name}_{tag}.png"3.3 图标资源在 UI 测试里的用途
源码根目录下有一批 PNG 文件,包括buy.png、checkout.png、login.png、logout.png等,这些在 UI 测试工程里通常有两种用途:一是作为测试用例里的预期图片,与页面截图做像素级对比;二是作为测试报告的图标资源,给日志和 HTML 报告增加可视化标记。从命名上看,这些图标对应商城系统的核心操作按钮,更可能是后者的角色,即被测试报告生成逻辑引用。如果你准备把这套框架接进 CI,建议把这类静态资源统一放到icons/子目录,避免根目录文件过多影响代码阅读。
4. 测试用例组织与命名规则:从文件名反推执行策略
4.1 unit / integration / combinatorial 的语义边界
testcases目录下每个开发者的子目录里都有test_unit01、test_integration02、test_combinatorial三类文件。这种命名不是随意起的,而是对应三种测试级别的约定。unit 测试验证单一功能点的最小行为,比如登录接口的密码校验;integration 测试验证模块之间的协作,比如“登录后进入购物车再结算”的跨页面流程;combinatorial 测试则聚焦参数组合,通常用多组数据覆盖同一业务规则分支。
| 级别 | 文件名标识 | 典型验证内容 | 执行频率 |
|---|---|---|---|
| 单元测试 | test_unit01 | 单个函数或方法的输入输出 | 每次提交 |
| 集成测试 | test_integration01 | 多个模块的交互流程 | 每日构建 |
| 组合测试 | test_combinatorial | 多参数组合的业务规则 | 版本发布前 |
pytest 的自动收集机制默认匹配test_*.py和*_test.py文件,这正是 testcases 目录能被直接扫描的原因。而 main.py 里的--level参数利用-k关键字过滤,本质上是把这套命名规则变成了“可寻址的测试分层工具”,不用维护额外的用例清单文件。
4.2 用 pytest 按命名规则执行分组用例
理解了命名规则后,运行指定类别的用例就变得非常直接。假设当前目录是项目根目录,常见的执行指令有:
# 运行单个开发者目录下的所有用例 pytest testcases/hjw -v # 运行所有集成测试 pytest testcases -k integration -v # 运行某文件中的特定用例 pytest testcases/hjw/test_integration01_hjw.py::test_add_to_cart -v --tb=short第一条命令按目录收集;第二条命令的-k integration只会匹配文件名里含 “integration” 的用例,也就是把四人目录下的集成测试全部收拢;第三条是精确到测试函数的定位,在排查单条用例问题时最常用。--tb=short参数能压缩回溯信息的输出量,只保留文件和行号级别的堆栈,当用例数量大、终端输出过长时,这个参数能让失败原因一眼定位。
4.3 test_user_defined.py:挂载非通用用例的扩展点
任何框架都会遇到“跑通用框架覆盖不了的特殊场景”,test_user_defined.py就是这个框架留下的扩展口,专门放带业务定制逻辑的用例。其设计意图在于:通用用例由数据驱动,而定制用例允许直接写逻辑。参考实际工程里的写法,自定义用例通常直接调用 UI.py 暴露的页面对象:
from UI import LoginPage def test_special_flow_with_coupon(driver): login_page = LoginPage(driver) login_page.login("vip_user_01", "test123") # 定制业务场景:使用优惠券结算 # 这里可以写专属的业务断言,不局限于通用登录结果 assert driver.title == "订单确认"这里暴露了框架设计的另一层意图:UI.py 中页面对象是和具体用例解耦的,自定义用例只需导入页面类即可复用既有封装,而无需触碰框架核心代码。如果你的项目里存在大量“只此一次”的临时验证需求,不建议直接堆进通用用例文件,而是全部收敛到test_user_defined.py,这样通用用例的执行结果不会被临时逻辑污染。
5. 跑通这套框架的关键细节与高频踩坑点
5.1 运行前必查的四个配置项
很多用例失败不是代码问题,而是前置配置没对齐。按这份源码的结构,我跑框架前会依次确认四点:
# 1. 浏览器驱动是否在 PATH 中 from selenium import webdriver driver = webdriver.Chrome() # 如果这里抛异常,说明 chromedriver 未安装或版本不匹配 # 2. config 下的 XML 配置里环境地址是否指向测试环境 # 检查 <baseUrl> 标签内容,避免连到生产环境 # 3. data 下的 CSV 编码是否为 UTF-8 # 用记事本另存为 UTF-8,避免中文字符乱码导致断言失败 # 4. 根目录是否已有 logs 目录 # controller.precheck() 会自动创建,但手动确认更稳妥浏览器驱动版本不匹配是 Selenium 自动化测试框架里出现频率最高的启动失败原因。Chrome 版本更新后,旧的 chromedriver 会直接抛SessionNotCreatedException,此时优先看报错信息里提示的 driver 版本号,而不是先怀疑框架代码。data目录下的 CSV 文件用 Excel 编辑后常被存成 GBK 编码,Python 读取时encoding="utf-8"就会抛UnicodeDecodeError——在open()里加errors="ignore"是临时救急,长期做法是统一编辑工具和编码标准。
5.2 用例失败时的定位链路
当一条 UI 用例报错时,我按这套框架的产物结构整理了一条固定排查链路:
- 看
logs/a.txt的运行日志,确认是走到哪一步才失败,日志末尾的 traceback 里会指明具体测试行。 - 看
screenshots目录下最新时间戳的 PNG,确认页面实际停留在什么状态——是弹窗遮挡、元素未加载,还是真的跳转错误。 - 对比用例里的断言值和数据文件里的
expected列,确认不是测试数据写错。
这条链路依赖框架的两个设计:日志里打印关键步骤名,截图带时间戳且不覆盖。如果你们的框架暂时没有日志,至少在截图命名里把用例名拼上,否则多条用例并发失败时根本对应不上。
5.3 接入持续集成时的最小改动点
这套框架要接入 Jenkins 或 GitLab CI,只需要在 main.py 的 pytest 参数里补充--junitxml报告输出:
python main.py --testcase testcases --level integration --html=report.html --junitxml=result.xmlJUnit XML 格式是 CI 平台原生识别的测试结果格式,而 HTML 报告更适合人工查看。两者同时输出,既满足机器的解析需求,也保留人的可读性。建议在 CI 的构建产物配置里把screenshots/和report.html都设为归档文件,这样每次流水线结束后,团队成员可以直接在构建页面里查看失败截图,不必登录服务器翻目录。这套源码虽然没有直接提供 CI 配置文件,但它的分层方式已经为这一步留好了接口——main.py 的参数化入口就是最自然的接入点。
本文还有配套的精品资源,点击获取