FEATURED · 精选文章

Python接口自动化测试框架源码解析:从分层设计到数据驱动实践

发布时间 / 2026/9/16 8:38:35
来源 / 创域科博编辑部
栏目 / 资讯中心
Python接口自动化测试框架源码解析:从分层设计到数据驱动实践 简介这是一份面向接口测试工程师与自动化测试初学者的Python接口自动化测试框架设计源码聚焦于从零搭建可复用、可扩展的测试体系解决用例管理、数据读取、请求封装、结果校验与报告发送等常见问题。压缩包共61个文件约525KB主要包含36个Python脚本、18张架构或流程图以及pytest.ini、YAML配置、ini配置与依赖清单等文件便于理解框架分层与配置驱动逻辑。目前已有1376人学习使用。资源内按common、config、datas、scripts、test_suite等模块划分涵盖数据库操作、Redis缓存、加密处理、日志记录、邮件与钉钉通知、随机数据生成、用例录制及新项目脚手架等工具脚本适合对照学习或直接改造为项目基础框架提升接口自动化落地效率。1. 接口自动化测试框架的源码结构从入口文件看设计接口自动化测试做到后期卡住团队的往往不是 requests 调用而是用例数据、执行入口和结果断言纠缠在一起要切换一个环境得翻几十个文件。这套基于 Python 的接口自动化测试框架源码用 pytest.ini、conftest.py、config、util、common、datas、testcase 把“配置、请求、校验、工具”切成独立层次适合需要长期维护的接口回归项目也适合从零搭建平台的团队研究分层边界。源码里既有 Requests 封装和 YAML 用例也包含随机数据、加密签名、多环境切换、邮件与钉钉通知等工程化细节。接下来按目录结构、运行入口、请求层、校验层的顺序拆解所有代码都可以改到自己的工程里直接复用。2. 框架分层与数据流pytest.ini、conftest.py如何拉起执行入口2.1 目录分层与职责边界先看整体布局pytest.ini 和 conftest.py 放在根目录pytest 启动时会自动加载testcase 内存放业务用例datas 内存放 YAML 测试数据common 是与业务无关的请求封装和断言util 及其 tools 子目录放通用工具scripts 是工程脚手架。这些边界不是随便画的如果随机数据生成逻辑放在 common那么所有业务包都依赖它一旦随机规则变化就要处处改动放到 util 后只有用到的地方才导入依赖方向更清晰。. ├── pytest.ini ├── conftest.py ├── requirements.txt ├── config/ │ ├── config.ini │ ├── confRead.py │ └── confManage.py ├── common/ │ ├── basePage.py │ └── checkResult.py ├── util/ │ ├── iniRequests.py │ ├── readYamlFile.py │ ├── datasTypeChange.py │ ├── randomData.py │ ├── encryption.py │ ├── log.py │ ├── caches.py │ └── tools/ ├── testcase/ │ └── demo/ │ └── setupMain.py ├── datas/ │ └── demo/ │ └── login.yaml └── scripts/ ├── newProject.py └── writeCase.py这个结构里config 层只做配置读取不参与请求util 层不依赖 pytestcommon 层依赖 util 但不依赖具体业务。test_suite 目录在项目里用来放按业务线组织的套件入口作用是把多条用例串成完整流程比如“创建订单-查询订单-取消订单”。各层典型文件与职责可以按表对应。| 目录 | 职责 | 典型文件 | | config | 多环境配置与读取入口 | config.ini, confRead.py, confManage.py | | common | 请求封装与断言校验 | basePage.py, checkResult.py | | util | 通用工具与底层能力 | iniRequests.py, readYamlFile.py, randomData.py | | testcase | 业务测试用例 | demo/setupMain.py | | datas | YAML 测试数据 | demo/login.yaml | | scripts | 项目脚手架与批量生成 | newProject.py, writeCase.py |分层的直接收益是“单人可写、团队可维护”新人只改 datas 里的 YAML 就能添加一条用例老手只改 common 或 util 就能调整全局行为不需要在用例层拼字符串。2.2 pytest.ini 决定用例收集范围pytest.ini 是整个框架的入口配置它决定了 pytest 从哪里找用例、带什么默认参数、注册哪些标记。常见配置如下[pytest] addopts -ra --tbshort -p no:cacheprovider testpaths testcase python_files test_*.py python_classes Test* python_functions test_* markers smoke: 冒烟用例 regression: 回归用例 p0: P0 级主流程addopts中的-ra让测试结束时汇总所有失败原因--tbshort让堆栈更短、便于快速定位-p no:cacheprovider关闭 pytest 缓存避免上次运行的.pytest_cache被误当成测试数据。testpaths testcase很关键它把收集范围限制在业务用例目录否则 scripts 下的脚手架代码如果出现test_开头的函数名会被当成测试用例收集。python_files与python_functions规定了用例文件与用例函数的命名模式这个项目全部采用 pytest 默认的test_*规范。| 配置项 | 作用 | | testpaths | 限定 pytest 扫描的用例目录 | | addopts | 注入命令行默认参数 | | markers | 注册 smoke、regression 等标记 | | python_files | 指定只能收集test_*.py文件 |2.3 conftest.py 注入全局 fixtureconftest.py 放在根目录pytest 会自动发现并且对 testcase 下所有子目录生效。它在这里承担两件事注册--env命令行参数并提供一个全局可见的env_configfixture。import pytest from config.confManage import ConfigManager def pytest_addoption(parser): parser.addoption( --env, actionstore, defaultdev, choices[dev, test, prod], help选择运行环境 ) pytest.fixture(scopesession, autouseTrue) def env_config(request): env request.config.getoption(--env) cm ConfigManager(env) yield cmpytest_addoption是 pytest 的钩子函数用例可以通过request.config.getoption(--env)拿到环境参数执行命令时写pytest --envtest即可切换。env_config使用scopesession同一个 pytest 进程内只初始化一次测试环境地址、超时时间这类全局变量不需要每个用例重复读文件。autouseTrue表示不显式声明也会加载但真正使用时最好还是写成参数避免隐式依赖导致用例意图不清晰。2.4 config.ini 与多环境读取config 目录下的 config.ini 是环境配置载体结构按 section 划分[dev] base_url http://127.0.0.1:8000 timeout 10 [test] base_url http://test.api.example.com timeout 5confManage.py 负责解析 iniconfRead.py 是对外暴露的读取入口。用 ConfigParser 读取时有一个容易踩的坑option 名默认会被转成小写如果 config.ini 里写BaseUrl http://...get(dev, BaseUrl)会报错必须写成get(dev, baseurl)。这个项目里统一使用小写键名规避了这个问题。注意ConfigParser 的getboolean(section, debug)只能识别 1/0/yes/no/true/false不要直接在 ini 里写debug True之外的值。数据流向是pytest.ini 决定收集范围conftest.py 注入 env_config用例通过 confRead.py 拿到 base_url再交给 common/basePage.py 发起请求。整个链路的配置读取方向是单向的每层只依赖相邻下层这也是这套源码最值得保留的设计边界。3. 请求执行层与YAML数据驱动basePage.py、iniRequests.py怎么配合3.1 为什么要把 requests 收敛到 basePage简单的接口脚本里直接requests.get(url, headers...)完全没问题但用例一旦多起来公共请求头、日志记录、超时控制、连接复用这些逻辑就会散落在几十个用例里。basePage.py 的价值在于把 requests 的调用统一收口用例只关心“调哪个接口、传什么参数、期望什么结果”不关心底层怎么拼 URL、怎么合并请求头。import requests from util.log import log from util.iniRequests import RequestFactory class BasePage: def __init__(self, base_url, headersNone): self.base_url base_url.rstrip(/) self.session RequestFactory.create_session() self.headers headers or {} def request(self, method, url_suffix, **kwargs): kwargs.setdefault(timeout, 10) merged_headers {**self.headers, **kwargs.get(headers, {})} kwargs[headers] merged_headers resp self.session.request( method.upper(), f{self.base_url}{url_suffix}, **kwargs ) log.debug(request %s %s, resp %s, method, url_suffix, resp.text[:500]) return resprstrip(/)先去掉 base_url 末尾斜杠避免拼接 URL 时出现http://host//api/login的双斜杠问题。setdefault(timeout, 10)保证所有请求默认带超时即使调用方忘记传 timeout也不会让用例无限等待。请求头合并采用{**self.headers, **kwargs[headers]}用例级请求头可以覆盖全局请求头的同名 key这是处理单个接口临时需要带头场景的常见做法。RequestFactory.create_session()负责创建requests.SessionSession 会复用 TCP 连接跑大批量用例时能明显减少握手耗时。3.2 iniRequests.py 与 iniHeaders.py 的分工util 目录下有两个容易混淆的模块iniRequests.py 负责请求对象的创建与初始化iniHeaders.py 负责请求头的组装。按项目内常见做法iniHeaders 从配置和登录接口获取 token再结合公共参数生成最终 headersiniRequests 则把这些 headers 注入到请求对象中。# util/iniRequests.py import requests class RequestFactory: staticmethod def create_session(): session requests.Session() session.headers.update({User-Agent: ApiTest/1.0}) return session这个工厂类看起来简单但它把 Session 的初始化收拢到一个地方。以后要接入连接池、添加统一的响应钩子只需要改这里。requests.Session()创建后headers 的默认值会作用于该 Session 的所有请求比在每个用例里手动传User-Agent干净得多。iniHeaders.py 里的init_headers(env)会读取 config.ini 中的 base_url、超时和账号配置再拼接上 token返回一个 dict。这样请求头构造规则变更时用例层完全不需要动。3.3 YAML 数据源与 parametrize 联动datas/demo/login.yaml 存放接口入参与期望结果readYamlFile.py 负责读取并转成 Python 字典。一个典型的登录用例数据如下login_cases: - title: 正常登录 body: username: admin password: 123456 expect: $.code: 0 $.data.user_name: adminreadYamlFile.py 内部使用yaml.safe_load而不是yaml.load避免 YAML 中恶意标签触发反序列化问题。读取之后再结合 pytest 的parametrize一条 YAML 数据就是一条用例。import pytest from util.readYamlFile import read_yaml from common.basePage import BasePage _CASES read_yaml(datas/demo/login.yaml)[login_cases] pytest.mark.smoke pytest.mark.parametrize(case, _CASES, ids[c[title] for c in _CASES]) def test_login(case, env_config): bp BasePage(env_config.get(base_url)) resp bp.request(post, /api/login, jsoncase[body]).json() for path, expected in case[expect].items(): assert _extract(resp, path) expectedids参数用来把 YAML 中的title显示成用例 ID这样失败报告里展示的是“正常登录”而不是一串参数拼接的匿名 ID。运行方式为pytest testcase/demo/test_login.py --envdev -m smoke。这里_extract(resp, path)的断言路径解析逻辑放在 checkResult.py下一章展开。3.4 randomData.py 与 datasTypeChange.py 处理动态参数接口测试不能全部用固定数据注册类接口对手机号唯一性有要求随机数模块就派上用场。randomData.py 里会提供随机手机号、随机邮箱、时间戳等生成函数。import random import time def random_mobile(): return 13 .join(random.choice(98765) for _ in range(9)) def current_timestamp(): return str(int(time.time()))这里有个容易被忽略的类型问题YAML 解析器会把裸数字解析成 int所以一段phone: 13800138000在 Python 里拿到的是整数而不是字符串。如果接口 JSON 里要求字符串类型直接json.dumps没问题但需要做数据比较时就会遇到类型不匹配。datasTypeChange.py 解决的就是这类转换把 int 手机号转字符串、把时间戳转 ISO 格式、把字符串布尔值转成 Python bool。项目里的经验是在构造请求体之前统一做转换不要在每个用例里临时str()。3.5 请求后清理与回归边界requestsTearDown.py 是容易被忽略的模块它的职责是请求后的资源清理。比如创建了订单后删除订单、登录后调用退出接口、注册后清理用户数据。放在 TearDown 而不是放到用例末尾是为了保证用例断言失败时清理逻辑仍然会执行。这个文件的存在意味着框架设计者意识到接口用例的核心不是单单发请求而是数据闭环。我在维护用例时也会把“清理是否干净”作为用例验收项否则回归几次后测试环境就会堆积大量脏数据。4. 多环境配置与结果校验confRead.py、setupMain.py、checkResult.py的配合4.1 confRead.py 与 confManage.py 的多环境读取confManage.py 负责把 config.ini 解析成可访问的对象confRead.py 则对外提供更短的读取入口。在实际工程里我会把 ConfigManager 设计成支持 fallback避免某个 environment 少配一个 key 时直接抛异常。from configparser import ConfigParser class ConfigManager: def __init__(self, env): self.parser ConfigParser() self.parser.read(config/config.ini, encodingutf-8) self.env env def get(self, key, fallbackNone): return self.parser.get(self.env, key, fallbackfallback) def get_int(self, key, fallback0): return self.parser.getint(self.env, key, fallbackfallback)使用方式是env_config.get(base_url)fallback 参数保证 undefined key 返回默认值而不是 None。None 在 URL 拼接时会被转成字符串None很难排查所以宁可让配置加载阶段直接失败也不要在请求阶段产生诡异的 URL。4.2 setupMain.py 的初始化入口testcase/demo/setupMain.py 在这个项目里承担测试集初始化的角色主要完成三类工作创建本次运行需要的报告目录、清理历史临时文件、初始化请求头。它通常不被 pytest 直接收集而是由 conftest 的 session fixture 调用或者作为模块显式执行。from util.mkDir import make_dirs from util.cleanFile import clean_file from util.iniHeaders import init_headers def setup_main(env): make_dirs([reports, logs/case_logs]) clean_file(datas/demo/temp, suffix.tmp) headers init_headers(env) return headersmake_dirs([reports, logs/case_logs])一次性创建多级目录避免后续写入报告时报 FileNotFoundErrorclean_file只清理指定后缀的临时文件防止误删 YAML 测试数据。init_headers(env)从配置中读取 base_url、token、公共参数组装成请求头返回后由 basePage 使用。setupMain 放在 testcase/demo 而不是根目录是因为不同业务线的初始化策略不同。支付线需要初始化商户密钥用户线需要读取登录态。setupMain.py 作为初始化入口能让业务线自己决定初始化逻辑。4.3 checkResult.py 的断言层设计校验层的核心问题是如何让断言表达得足够简洁又能在失败时给出可读信息。checkResult.py 采用点路径取值把 JSON 响应中的$.code、$.data.user_name这类字符串转换成实际值然后与期望值比较。def _extract(data, path): if path.startswith($.): path path[2:] cur data for key in path.split(.): if isinstance(cur, list) and key.isdigit(): cur cur[int(key)] else: cur cur[key] return cur def check_result(resp_json, assertions): for path, expected in assertions.items(): actual _extract(resp_json, path) assert actual expected, f{path} 期望 {expected}实际 {actual}_extract支持形如$.data.order_info.0.order_id的路径数字 key 会按数组下标处理。为什么不用现成的 JSONPath 库原因是绝大多数业务接口的响应结构只有两层到三层点路径已经能覆盖大部分校验场景JSONPath 的递归匹配在大响应体上会有额外性能开销而且报错信息不如点路径直观。对于列表长度、枚举值比较这类特殊断言可以在 checkResult.py 里追加单独的函数不建议把复杂的业务判断写到用例层。4.4 失败定位与通知链路log.py 负责记录每个请求的 method、URL、请求体、响应体前 500 字符方便用例失败后打开日志定位是入参问题还是接口问题。dingding.py 和 sendEmail.py 是两种通知渠道CI 上跑完用例后把失败摘要推到钉钉群或发邮件给负责人。常见配置如下[notification] mail_to qaexample.com mail_subject 接口回归失败报告 dingtalk_webhook https://oapi.dingtalk.com/robot/send?access_tokenxxx通知要解决的问题不是“让大家都知道”而是让失败信息第一时间触达能处理的人。日志记录到文件通知只发送摘要和失败用例名这两件事要分开做不要在通知里贴几十行堆栈。5. 批量生成用例与跨用例依赖newProject.py、caches.py与redisData.py5.1 newProject.py 与 writeCase.py 快速生成用例新接口接入时手动创建 testcase、datas、setupMain 三个文件很烦琐。scripts/newProject.py 的作用是按模板生成一个完整的新模块writeCase.py 则根据已录入的 YAML 描述生成 pytest 用例代码。python scripts/newProject.py --module order --case create_order执行后会在 testcase/order/ 下生成 test_create_order.py在 datas/order/ 下生成 create_order.yaml。模板里只保留 BasePage 调用和 checkResult 校验骨架具体接口路径、入参、期望值由 YAML 补全。两个脚本的共同点是“生成代码可读、可改”而不是生成后不可维护的魔改样板。recording.py 则是辅助录制入口把手工请求记录转成 YAML 用例格式。接口还没完全文档化时从访问日志里捞一条真实请求转成标准数据格式比人工手写 YAML 更快也更容易保持字段完整。5.2 caches.py 做接口间数据依赖接口自动化里常见的依赖链是创建订单返回 order_id再用 order_id 去查询。caches.py 提供了一个进程内缓存用于这类跨用例数据传递。from util.caches import Cache cache Cache() def test_create_order(env_config): order_id create_order() cache.set(current_order_id, order_id) def test_query_order(env_config): order_id cache.get(current_order_id) resp query_order(order_id) assert resp[code] 0进程内缓存要解决的问题是跨函数传参而不是替代 Redis。pytest 的用例执行顺序默认不保证所以两个用例强依赖顺序时需要显式排序或者把创建与查询放到同一个用例里执行。caches.py 处理的是同一进程内的动态数据redisData.py 处理的是从 Redis 读取的预置数据两者不要混用。5.3 拿到源码后的第一次验证readme.txt 里说明了环境依赖先执行pip install -r requirements.txt安装 requests、pytest、PyYAML 等依赖然后跑通 demo 用例pip install -r requirements.txt pytest testcase/demo --envdev -m smokedemo 用例通过后再去改 datas/demo/login.yaml 里的预期值让它失败一次观察 checkResult.py 的断言信息是否准确。源码包里附带多张 png 截图主要是 demo 运行后的目录与报告形态首次阅读时对照 readme.txt 会更清楚。整个流程在项目根目录执行pytest --collect-only只能看到用例名称想看实际请求是否通加--log-cli-levelDEBUG跑一遍 demo。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻