恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Python+requests+pytest:打造可扩展的接口自动化测试框架
首页
资讯中心
/
Python+requests+pytest:打造可扩展的接口自动化测试框架
Python+requests+pytest:打造可扩展的接口自动化测试框架
发布时间:2026/9/16 9:32:32
简介这是一套面向中高级测试开发者的接口自动化框架封装资源聚焦数据类型处理与DDT数据驱动设计。资源通过示例演示如何将${read_extract_data(tag_id)}动态参数替换为实际值并对比excel、csv、yaml作为数据源的优缺点重点讲解基于yamlddt的轻量数据驱动方案适合正在搭建或优化pytest接口测试项目的工程师参考。压缩包共142个文件约2.06MB。其中17个Python脚本负责框架核心与用例执行12个yaml文件用于配置及数据驱动场景70个json文件承载接口参数与预期结果另有csv、xml、html等辅助资源整体目录结构清晰便于按模块学习和复用到自己的项目中。目前已有4440人学习内容围绕接口自动化框架的封装实践展开。除代码和样例数据外还附有接口自动化测试规范与测试用例规范说明可帮助读者理解框架设计思路快速落地数据驱动改造提升接口测试的维护效率。1. 用 pythonrequestspytest 搭接口自动化框架为什么这个组合八年了还绕不开2017 年我用 unittest 写第一版接口自动化的时候被同事问为什么不直接用 Postman 导出脚本。当时答不上来后来被回归集膨胀、环境切换、失败定位三件事反复教做人才明白 Postman 帮你省下的时间会在用例组织这个环节连本带利还回来。pythonrequestspytest 这套组合能活这么久核心是它把发请求和组织用例拆得足够开requests 负责 HTTP 层的全部细节pytest 负责用例发现、执行、夹具和结果汇总你只需要把中间那层业务封装写干净。标题里的 (8)我理解为迭代到第 8 版的工程产物有目录规范、有公共封装、有报告输出而不是随手堆的脚本集。下文按从零搭到能扛住大规模回归的路径把每一层的设计取舍、参数和踩坑点讲透。适合准备重建测试基建或想把散落脚本结构化的读者。2. requests 会话层先把请求封装做对pytest 才接得住2.1 requests 会话封装最小代码Session、连接池与统一头常见做法是先建一个 api 包里面放 base.py核心是一个基于 requests.Session 的客户端类。Session 和直接 requests.get 的最大区别在持久连接同一个 Session 内的 TCP 连接会复用HTTP keep-alive 生效。接口测试跑上千条用例时Session 复用的收益不只是快还在于连接数统计不会忽高忽低不至于每条用例都重新走一遍 TCP 握手和 TLS 协商。# api/base.py import requests from requests.adapters import HTTPAdapter class ApiClient: def __init__(self, base_url: str, token: str | None None): self.session requests.Session() self.session.headers.update({ Content-Type: application/json, User-Agent: pytest-api-framework/8.0, }) if token: self.session.headers[Authorization] fBearer {token} self.base_url base_url.rstrip(/) # 连接池配置最多 10 个连接池单主机并发 20 adapter HTTPAdapter(pool_connections10, pool_maxsize20) self.session.mount(https://, adapter) self.session.mount(http://, adapter) def get(self, path: str, **kwargs): return self.session.get(self.base_url path, **kwargs) def post(self, path: str, json: dict | None None, **kwargs): return self.session.post(self.base_url path, jsonjson, **kwargs)这段代码做了四件事统一请求头、注入鉴权、配置连接池、约束路径拼接。逻辑上Session 对象在进程内长期存活所有用例共用因此任何在 session 上设置的 headers 都会自动带到后续请求。rstrip(/)防止调用方写/api/v1/users而 base_url 以/结尾时拼出双斜杠——这行在手工脚本里无所谓在框架里是排障第一名的隐藏雷。参数说明pool_connections10 表示 Session 针对不同主机的连接池数量上限pool_maxsize20 表示单个主机的连接复用上限。被测服务只有一个域名时起作用的其实是 pool_maxsize多域名走同一框架时才需要 pool_connections。并发低于 50 时这两个值不用动并发上百时反而要调小否则本机文件描述符先被耗尽。2.2 环境切换base_url、超时与重试参数集中配置接口自动化最容易翻车的场景是本地跑得好好的一上 CI 就 404。根源大多是环境地址散落在用例代码里线上地址复制得遍地都是。框架级解法是把环境配置集中在 YAML按环境变量加载。# config/environments.yaml dev: base_url: http://127.0.0.1:8000 timeout: 5 retry_times: 0 staging: base_url: https://api.staging.example.com timeout: 10 retry_times: 3 prod: base_url: https://api.example.com timeout: 15 retry_times: 0# config/env_config.py import os import yaml def load_env_config(default_env: str dev) - dict: env os.getenv(API_ENV, default_env).lower() with open(config/environments.yaml, encodingutf-8) as fp: all_cfg yaml.safe_load(fp) if env not in all_cfg: raise ValueError(funknown env: {env}, available: {list(all_cfg)}) return all_cfg[env]调用方式是API_ENVstaging pytest -q tests/fixture 里执行一次 load_env_config整个测试进程共享。逻辑说明先读环境变量读不到则回退 dev环境名校验失败直接抛异常避免部署到未知环境。参数说明每个环境的 timeout 独立设置dev 给 5 秒是因为本机接口响应最快staging 给 10 秒是因为有网关和日志中间层生产给 15 秒是留足慢查询的余量。retry_times 我特意让 staging 与 prod 不同——staging 常有限流和网络抖动放开重试能减少误报生产写接口默认 0。这个文件里还可以放每个环境的专用账号、签名密钥、是否开启响应录制。注意密钥不要提交到 Git用*.local.yaml覆盖机制或 CI 注入。2.3 响应统一结构断言层不必到处 try/exceptrequests.Response 灵活但太灵活。用例里到处写resp.json()[data][list][0][id]服务端调整字段层级就要改十几个文件。常见做法是先包一层响应结构# api/response_wrapper.py from dataclasses import dataclass from datetime import datetime dataclass class ApiResponse: status_code: int elapsed_ms: int body: dict | list | str | None request_path: str request_params: dict | None timestamp: str def wrap_response(resp) - ApiResponse: try: body resp.json() except ValueError: body resp.text return ApiResponse( status_coderesp.status_code, elapsed_msint(resp.elapsed.total_seconds() * 1000), bodybody, request_pathresp.request.path_url, request_paramsgetattr(resp.request, params, None), timestampdatetime.now().isoformat(), )在 2.1 节 client.get 和 client.post 的 return 处调用 wrap_response用例层拿到的就是 ApiResponse。断言可以写成def test_get_user_detail(client, env_cfg): r client.get(/api/v1/users/10086) assert r.status_code 200 assert r.body[code] 0 assert r.elapsed_ms int(env_cfg[timeout]) * 1000逻辑说明wrap_response 先把 JSON 解析结果放进 body解析失败降级为原始文本而非抛异常目的是避免 502 文本响应被解析异常吞掉真实错误。elapsed_ms 是 requests 内部从发起到收到响应的净耗时不含 pytest fixture 的 setup/teardown比用例级 wall time 更适合做接口性能基线。request_path 和 request_params 是为后面的日志与失败还原留的数据现在用不上排障时会感谢这两个字段。封装层职责常见翻车点ApiClient连接池、公共头、鉴权、路径拼接base_url 末尾斜杠、超时不传env_config多环境地址、超时、重试阈值环境名写错、密钥误入库ApiResponse响应解析、耗时与请求上下文JSON 解析异常未兜底3. pytest 夹具与参数化让用例从 50 条涨到 2000 条不失控3.1 conftest.py 里的 fixture 作用域session、module、function 怎么选pytest 的 fixture 是接口自动化框架里用得最重、也最容易被误解的机制。作用域决定生命周期接口自动化核心就两种session 级给 ApiClientmodule/function 级给测试数据。新手常犯的错误是把 token 获取也放 session 级——token 有效期只有 30 分钟全量回归要跑 40 分钟后半场全是 401重跑整个回归的代价全算在一个人头上。# conftest.py import pytest from api.base import ApiClient from utils.token_manager import get_fresh_token pytest.fixture(scopesession) def client(): 整个测试会话共享一个 ApiClient测试结束后关闭连接池。 token get_fresh_token() c ApiClient(base_urlhttp://127.0.0.1:8000, tokentoken) yield c c.session.close() pytest.fixture(scopemodule) def user_ctx(client): 每个测试模块独立创建测试用户模块跑完删除避免数据残留。 resp client.post(/api/v1/test/users, json{type: random}) assert resp.status_code 201, resp.body user_id resp.body[data][id] yield {uid: user_id} client.delete(f/api/v1/test/users/{user_id})逻辑说明client 是 session 级唯一要注意的是 yield 后必须session.close()否则进程退出前连接池一直占着系统 fd在 pytest-xdist 并发跑的时候不 close 的连接会成倍堆积。user_ctx 是 module 级作用是在一个模块内共享同一份测试用户数据模块结束后删掉避免测试库被反复创建的数据撑爆。选型口诀环境上下文和请求客户端用 session模块内共享且可重建的数据用 module用例之间不共享的祭品数据用 function。三者不是层级包含取值的核心看数据可变性可变数据不因跨模块共享而互相干扰的才允许往上提作用域。作用域生命周期适合不适合session整个 pytest 进程连接池、token、全局配置会在用例间变更的业务数据module单个模块模块级共享测试数据跨模块依赖共享function每条用例独立隔离的数据准备创建成本极高的数据3.2 pytest.mark.parametrize 驱动 CSV 数据和接口参数数据驱动是 pytest 里最直接提升用例密度的手段。一次参数化声明CSV 里每行就是一个用例新增测试场景只需要改数据文件不用碰代码。看一个最小实现# test_cases/test_user_api.py import pytest from utils.data_loader import load_csv pytest.mark.parametrize( case, load_csv(data/user_cases.csv), idslambda c: c[case_id], ) def test_create_user(client, case): resp client.post(/api/v1/users, json{ name: case[name], email: case[email], role: case[role], }) assert resp.status_code int(case[expected_code]) if resp.status_code 200: assert resp.body[data][id] 0数据文件 user_cases.csvcase_id,name,email,role,expected_code USR-001,alice,alicetest.dev,admin,200 USR-002,bob,bobtest.dev,viewer,200 USR-003,carol,caroltest.dev,,400ids 参数把 pytest 显示的用例名从test_create_user[case0]变成test_create_user[USR-001]在 pytest 报告、allure 报告里直接对应需求编号省掉人工映射。load_csv 内部用 csv.DictReader 解析字段缺失时应该在加载阶段抛异常而不是到断言时才报 KeyError——加载时报错者是写数据的同事断言时报错者是排查的你这一点分清了团队协作效率完全不同。parametrize 的进阶用法是和 marker 组合数据文件里加一列is_smoke在用例内部解析后动态添加 marker 比较麻烦通常做法是拆成两个测试函数或再建一个筛选用的 CSV 列配合-m过滤。别为了省函数数量把标记逻辑写进测试体内可读性会断崖式下降。3.3 冒烟与全量回归marker 与 addopts 配置用例上千之后你不可能每次提交都跑全量。pytest marker 机制用来做用例分层先配置文件# pytest.ini [pytest] markers smoke: 冒烟用例每次提交必跑 regression: 全量回归 slow: 执行超过 10 秒的用例需要外部依赖 addopts -m not slow -p no:cacheprovider testpaths testsmarkers 块声明三个标签并写注释addopts指定默认执行条件和关闭缓存。-m not slow的含义是默认跳过带 slow 标记的用例只有显式pytest -m slow时才跑它们这个默认值的逻辑是慢用例多半依赖外部服务不该成为常规提交的阻塞项。-p no:cacheprovider不是必须参数但当 CI 运行在只读文件系统时它避免.pytest_cache写入失败导致的整批报错。代码层面给用例打标pytest.mark.smoke def test_login_smoke(client): resp client.post(/api/v1/login, json{user: admin, pass: x}) assert resp.status_code 200 pytest.mark.regression def test_complex_order_flow(client): ...命令行切片方式pytest -m smoke --collect-only -q # 先确认冒烟集只有 12 条防止误标 pytest -m smoke -q # 提交前快速回归 pytest -m regression and not slow -n 4 # 全量回归跳过慢用例4 进程并发第一个命令里的--collect-only是调试利器它只收集用例不执行输出用例名清单用于核对 marker 是否标对了。第三个命令里-n 4来自 pytest-xdist 插件注意 xdist 与 session 级 fixture 的交互——session fixture 会把初始化代码在每个 worker 里各执行一次token 获取、连接池创建都要保证线程安全。4. pytest 报告与 requests 重试把 429 限流和失败现场一次说清4.1 requests 重试写在哪层urllib3 Retry 还是 pytest-rerunfailures线上服务平时正常CI 并发一高接口测试里就会出现 429、503 这类瞬时错误。这时在用例层手写while True重试是下策重试逻辑散落各处失败后无法统一调整而且会把真实的服务故障掩盖成重试后通过的报告。框架里有一层正解是在 urllib3 的 Retry 里做挂到 requests 的 adapter 上# api/retry_policy.py from urllib3.util.retry import Retry from requests.adapters import HTTPAdapter def build_retry_adapter(retry_times: int 2, backoff: float 0.5) - HTTPAdapter: retry_strategy Retry( totalretry_times, connectretry_times, readretry_times, backoff_factorbackoff, status_forcelist[429, 500, 502, 503, 504], allowed_methodsfrozenset([GET, HEAD, OPTIONS]), ) return HTTPAdapter(max_retriesretry_strategy)把 2.1 节里直接创建的 HTTPAdapter 换成这个函数的返回值即可。参数说明total 限制总重试次数不含第一次请求connect 和 read 分别限制连接失败、读取超时的重试次数status_forcelist 列出需要重试的状态码集合allowed_methods 故意不含 POST/PUT/DELETE是防止写操作被重放导致业务侧重复扣款、重复下单。backoff_factor0.5 的退避序列是 0.5s、1s、2s公式是 backoff_factor 乘以 2 的重试次数减一次方。第二层是 pytest 框架级的 pytest-rerunfailurespip install pytest-rerunfailures pytest -q --reruns 2 --reruns-delay 3 --only-rerun 429|500|503|timed out--reruns 2表示用例失败后最多重跑两次--reruns-delay 3是两次重跑间隔 3 秒--only-rerun 429|500|503|timed out是正则表达式只有匹配到的失败信息才触发重试其他失败直接判失败。注意这个正则匹配的是失败阶段的 stdout 文本不是 HTTP 状态码本身所以写法上要包含状态码数字或关键词。我一般这样分工requests 层 Retry 处理单次请求的瞬时抖动不产生新的用例记录pytest-rerunfailures 处理用例依赖数据还没最终一致的场景并且报告会保留重跑痕迹。审计严格的项目宁可让 pytest 层留下失败后重试的记录也不要在 requests 层无限重试。4.2 429 too many requests 的处理Retry-After 与并发步长最近接口自动化群里高频出现的报错是exceeded retry limit, last status: 429 too many requests伴随的还有too many concurrent requests、stream disconnected before completion这类网关文案。这类报错传达两个信息请求方把上游限流打满了且重试逻辑没有正确处理 429 的 Retry-After 响应头。提示很多网关对 429 的语义不是过一会儿再试而是你已经超过了我的配额。盲目重试只会让限流窗口延长。正确的处理是让 urllib3 读懂 Retry-Afterretry_strategy Retry( total3, status3, status_forcelist[429], backoff_factor0, respect_retry_after_headerTrue, )respect_retry_after_header 在 urllib3 里默认是 True但如果项目里有人显式传过 False或者升级依赖时被顺手优化过就必须写回来。行为是服务端响应头里有 Retry-After秒数时按它等待没有这个头才走 backoff_factor 指数退避。backoff_factor0 是为了让 Retry-After 成为唯一等待来源避免两者叠加成一次重试等十几秒。并发层也要收着打用 -n 和 --maxfail 配合pytest -n 8 --maxfail 5 -q tests/api_v1/test_order.pymaxfail5 的含义是每个 worker 累计失败 5 次后该 worker 退出。这在限流场景下等于保险丝上游开始 429 时用例快速失败与其让 8 个 worker 继续打同一个已经被打满的服务不如尽早收摊人工介入看限流策略。注意 maxfail 是全局参数别写进 addopts否则全量回归里某个模块出问题会提前终止整个测试会话。失败场景应该用不应该用原因读接口瞬时连接超时urllib3 Retrypytest-rerunfailures连接层抖动不污染报告业务断言失败都不重试任何重试重试会掩盖真实回归上游限流 429/503两层配合只依赖某一层Retry 处理语义rerun 通知执行者4.3 allure 报告与日志失败现场可还原框架迭代到后期拼的是排障效率。allure 报告提供标题、标签、步骤和附件四级能力接口测试至少要用上前三项import allure from utils.report_helper import attach_request_response allure.title(创建用户[{case_id}]) allure.tag(user, regression) allure.severity(allure.severity_level.CRITICAL) def test_create_user(client, case): resp client.post(/api/v1/users, json{...}) attach_request_response(resp) with allure.step(校验状态码): assert resp.status_code int(case[expected_code])attach_request_response 的实现如下# utils/report_helper.py import allure import json def attach_request_response(resp): allure.attach( bodyjson.dumps(resp.body, ensure_asciiFalse, indent2)[:4000], namef{resp.request_path} response, attachment_typeallure.attachment_type.JSON, )body 截断到 4000 字符是防止超大响应把报告 HTML 撑爆。日志方面logging 模块比 print 可靠因为 pytest 会对 print 输出做捕获断言失败时才能看到它而 logging 可以按级别独立控制import logging logger logging.getLogger(api_framework) def log_request_response(resp): logger.info( HTTP %s in %dms | path%s, resp.status_code, resp.elapsed_ms, resp.request_path, )在 wrap_response 返回前调用一次日志里就有了完整的请求-响应链。命令行加--log-cli-levelINFO能看到行内输出配合-v看用例与请求的一一对应关系排查哪条用例打到了哪个接口会快很多。5. 用 pytest 收集 hook 生成 API 覆盖矩阵防止第 9 版代码腐化5.1 把覆盖矩阵挂上 CI 门槛diff 非零即失败框架迭代到第 8 版比能不能跑更焦虑的是哪些接口没有被覆盖到。pytest 的 pytest_collection_modifyitems hook 在用例收集完成后、开始执行前触发是生成覆盖矩阵的合适位置# conftest.py import json def pytest_collection_modifyitems(session, config, items): 收集完成后输出接口覆盖矩阵到 artifacts 目录。 matrix {} for item in items: module getattr(item, module, None) if module is None or not module.__name__.endswith(_api): continue api_tag module.__name__.split(.)[-1] # 例如 order_api entry matrix.setdefault(api_tag, {count: 0, markers: []}) entry[count] 1 for marker in item.iter_markers(): if marker.name not in entry[markers]: entry[markers].append(marker.name) with open(artifacts/coverage_matrix.json, w, encodingutf-8) as fp: json.dump(matrix, fp, ensure_asciiFalse, indent2, sort_keysTrue)运行pytest --collect-only -q即可生成。矩阵里每个 API 模块的用例数和 marker 清单能一眼看出哪个接口只有冒烟没有回归、哪个模块整块缺失。前提是测试文件的模块名遵循api_api.py命名规范比如order_api.py对应订单接口——这个约束反过来倒逼你的文件结构保持整洁。CI 门槛可以这样挂pytest --collect-only -q /tmp/collected.txt grep -Eo test_[a-z_]_api /tmp/collected.txt | sort -u /tmp/covered.txt diff /tmp/covered.txt scripts/required_api_list.txtgrep -Eo提取测试模块名然后排序去重与后端提供的 required_api_list.txt 做 diffdiff 非空即返回非零退出码pipeline 直接红。逻辑说明required_api_list.txt 由后端在每次接口上线时同步维护覆盖所有已上线 API 模块。团队协作里这个门槛的价值是硬约束——每个新接口必须配套测试文件否则无法发布而不是靠周会上提醒记得补用例。最后落地时有个习惯可以带上覆盖矩阵不只 CI 里看本地pytest --collect-only后顺手打开看一眼它比翻一遍测试报告更快发现问题。写新接口用例时对照矩阵里 count 最小的模块优先补比凭感觉挑着写更有的放矢。本文还有配套的精品资源点击获取