
在接口自动化测试领域Pytest 和 Requests 是 Python 生态中组合频率非常高的一对工具。Requests 负责把 HTTP 请求发出去并拿到响应Pytest 负责把用例组织起来、执行断言、生成报告。很多人搭框架时最困惑的不是某一条用例怎么写而是整个工程该按什么结构组织配置放哪里、公共方法放哪里、测试数据怎么管理、日志怎么落盘、环境切换怎么做。这篇文章会从一个最小可用用例开始逐步搭出一个适合中小团队使用的接口自动化测试框架并给出常见报错的排查路径。1. 先理清 Pytest 和 Requests 在接口自动化框架里各自负责什么接口自动化测试框架并不是“Requests 加 Pytest”两个库拼在一起这么简单。先搞清楚两个库的边界后面设计目录和封装时才不会混淆职责。1.1 Requests 让 HTTP 请求发送变得可控Requests 是一个 HTTP 客户端库解决的是“如何把一个 HTTP 请求发送出去并把响应读回来”的问题。它不关心测试用例是否通过也不负责生成测试报告。最基本的用法是这样import requests resp requests.post( https://httpbin.org/post, json{username: tester, password: 123456}, timeout10, ) print(resp.status_code) print(resp.json())这段代码能做三件事构造一个 POST 请求并指定 JSON 请求体。等待服务端返回响应。把响应状态码和 JSON 内容打印出来。但它没有断言没有测试组织能力也不会告诉你“这条用例失败在哪一步”。也就是说只靠 Requests 写出来的叫“请求脚本”不叫“测试框架”。Requests 在框架中的定位是协议层。它负责统一管理请求头、超时时间、连接复用、Cookie 和认证信息。这一层做得好用例层就不需要每次请求都重复写requests.post(...)。1.2 Pytest 提供测试组织和断言能力Pytest 是测试框架负责把请求脚本变成可执行的测试用例。它提供四个核心能力用例识别文件名、函数名、类名满足规则时自动收集。断言直接使用 Python 的assert失败时输出详细信息。夹具fixture提供初始化、清理、共享对象的能力。插件通过 pytest-html、pytest-rerunfailures、allure-pytest 扩展报告和重试能力。最小示例def test_add(): assert 1 1 2运行pytest -vPytest 会找到test_开头的函数并执行。断言失败时它会显示表达式两边的值方便定位问题。在接口自动化中Pytest 负责的是用例层和调度层谁先执行、哪些用例需要登录态、哪些用例属于冒烟集、失败后是否重试、最终生成什么报告。Requests 不关心这些Pytest 也不关心 HTTP 细节两者配合才完整。1.3 脚本和框架的本质区别单脚本的问题在于所有逻辑堆在一起。写三条用例时还能忍受写到几十条用例时会出现明显痛点环境地址写死、数据无法复用、没有日志、失败后不知道请求到底发出去没有、换一套环境要改所有代码。框架化则是把职责拆成层次层次职责典型模块用例层描述业务场景断言响应结果testcases/业务封装层提供登录、下单等场景方法core/协议层统一请求发送、超时、请求头core/api_client.py数据层管理测试数据和环境配置data/、config/报告层日志、报告、执行结果汇总pytest 插件框架化不是引入多少高级概念而是让每段代码只做一件事。后面所有章节都围绕这个分层展开。2. 环境准备与项目骨架目录结构决定框架能不能扩散接口自动化框架的目录结构不是形式主义。目录分得不清楚配置、数据、用例、公共模块最终会混在一起项目越大越难维护。2.1 Python 版本、虚拟环境和依赖管理建议使用 Python 3.9 及以上版本。Pytest、Requests 对 Python 3.9 以上版本支持稳定类型注解和 f-string 等特性也更好用。创建虚拟环境python -m venv .venv激活虚拟环境# Linux / macOS source .venv/bin/activate # Windows .venv\Scripts\activate激活后命令行提示符前会出现(.venv)。这一步很重要接口自动化环境的依赖必须和系统其他项目隔离否则不同项目对同一依赖的版本要求会互相干扰。2.2 推荐目录结构推荐按以下结构组织项目api_test/ ├── config/ │ ├── config.yaml │ └── config.py ├── core/ │ └── api_client.py ├── data/ │ └── login_cases.yaml ├── testcases/ │ ├── conftest.py │ └── test_login.py ├── utils/ │ ├── logger.py │ └── yaml_loader.py ├── requirements.txt ├── pytest.ini └── run.py各目录职责目录或文件职责config/环境配置文件和配置读取类core/请求封装、核心客户端data/接口测试数据文件testcases/测试用例和 conftest.pyutils/日志、文件读取等工具requirements.txt依赖列表pytest.iniPytest 配置run.py可选统一执行入口这个结构的特点是用例层不直接依赖 Requests 细节config 层不关心用例内容数据文件只描述输入输出。后续扩展 CI、新增接口、切换环境都只改对应层。2.3 requirements.txt 依赖说明在项目根目录创建requirements.txtrequests2.32.3 pytest8.2.1 PyYAML6.0.1 pytest-html4.1.0 pytest-rerunfailures14.0安装pip install -r requirements.txt这里把版本写死是为了保证团队和 CI 环境执行结果一致。如果原始安装时不知道具体版本可以先不写版本号安装再用pip freeze requirements.txt生成锁定版本。常用依赖用途依赖用途说明requests发送 HTTP 请求框架核心pytest测试框架用例组织与执行PyYAML解析 YAML 数据存储配置和测试数据pytest-html生成 HTML 报告方便本地查看pytest-rerunfailures失败用例重试处理偶发网络问题2.4 最小环境校验创建最小用例文件testcases/test_demo.pydef test_demo_ok(): assert 1 1执行pytest testcases/test_demo.py -v预期输出包含testcases/test_demo.py::test_demo_ok PASSED如果这一步失败先不要继续写用例优先检查 Python 解释器是否来自虚拟环境、依赖是否装全、路径是否进入项目根目录。环境没跑通之前后面所有框架代码都无法验证。3. 从最小用例到框架化配置、封装与夹具很多教程直接给一整套封装代码读者复制下来却不知道为什么要这样分。这一章从一条最原始的 Requests 用例开始一步步改成框架结构。3.1 用 Requests 直接写一条用例先写一条没有封装、没有配置的原始用例import requests def test_login(): resp requests.post( https://httpbin.org/post, json{username: tester, password: 123456}, timeout10, ) assert resp.status_code 200 assert resp.json()[json][username] tester这条用例能跑通但有三处不好维护base_url写死在用例里换环境要改代码。超时时间写死不能在配置中统一调整。每个接口都要手动调requests.post如果后续要统一加日志、加请求头需要改所有用例。3.2 fixture 管理 base_url 和 Session先用 Pytest fixture 把客户端提取出来。这里引入requests.Session()它比直接调用requests.post更有优势复用底层 TCP 连接请求效率更高。可以统一保存 Cookie登录态不用每次手动处理。可以统一设置 headers例如Authorization。改进后的 conftest.pyimport pytest import requests pytest.fixture(scopesession) def client(): session requests.Session() session.base_url https://httpbin.org yield session session.close() def test_login(client): resp client.post( client.base_url /post, json{username: tester, password: 123456}, timeout10, ) assert resp.status_code 200 assert resp.json()[json][username] testerscopesession表示整个测试会话只创建一次客户端。如果每个用例都创建一个新 Session就失去了连接复用和登录态共享的意义。但这里仍有问题base_url还是硬编码在 fixture 里。下一步引入配置文件。3.3 配置管理用 YAML 支持多环境切换创建config/config.yamltest: base_url: https://httpbin.org timeout: 10 prod: base_url: https://api.example.com timeout: 15创建config/config.pyfrom pathlib import Path import yaml class Config: def __init__(self, envtest): config_path Path(__file__).parent / config.yaml with open(config_path, r, encodingutf-8) as f: self._data yaml.safe_load(f) if env not in self._data: raise ValueError(funknown env: {env}) self._env env property def base_url(self): return self._data[self._env][base_url] property def timeout(self): return self._data[self._env][timeout]这样配置读取被统一收口。以后新增环境只增加 YAML 对应节点不需要改用例。实际生产项目还可以增加环境变量覆盖逻辑import os env os.getenv(API_ENV, test) config Config(env)API_ENV是约定名称CI 里可以通过环境变量注入test、staging或prod。配置文件的职责是提供默认值环境变量负责覆盖运行时环境。3.4 统一请求封装日志、超时与异常兜底有了配置还需要一个统一的请求客户端让用例层不再关心 URL 拼接和超时设置。创建core/api_client.pyimport logging import requests from config.config import Config logger logging.getLogger(api) class ApiClient: def __init__(self, envNone): self.config Config(env) self.session requests.Session() self.base_url self.config.base_url def request(self, method, path, **kwargs): url path if path.startswith(http) else self.base_url path kwargs.setdefault(timeout, self.config.timeout) logger.info(request: %s %s, method.upper(), url) logger.info(params: %s, kwargs.get(params)) resp self.session.request(method, url, **kwargs) logger.info( response status: %s, body: %s, resp.status_code, resp.text[:1000], ) return resp def get(self, path, **kwargs): return self.request(GET, path, **kwargs) def post(self, path, **kwargs): return self.request(POST, path, **kwargs) def close(self): self.session.close()关键点setdefault(timeout, ...)表示调用方传了 timeout 就用调用方的值没传才用配置默认值。路径以http开头时直接使用完整地址否则拼接base_url。日志记录请求方法和 URL响应体只记录前 1000 个字符避免大响应刷屏。改造后的用例from core.api_client import ApiClient def test_login(client): resp client.post(/post, json{username: tester, password: 123456}) assert resp.status_code 200 assert resp.json()[json][username] tester现在用例层不再关心环境地址、超时、连接管理只描述“发什么请求、断言什么结果”。4. 把测试数据从用例代码中剥离出来参数化与数据驱动接口自动化框架做到一定程度瓶颈通常不在请求发送而在用例数据的组织和维护。把数据从用例代码里剥离是降低维护成本的关键。4.1 parametrize 参数化的三种常见用法最简单的参数化是直接在函数上标记import pytest pytest.mark.parametrize(username,password,expected, [ (tester, 123456, 200), (, 123456, 200), ]) def test_login_param(client, username, password, expected): resp client.post(/post, json{username: username, password: password}) assert resp.status_code expectedPytest 会把每个元组生成一条独立用例失败时只看对应数据即可。第二种用法是传入列表适合参数本身就是字典pytest.mark.parametrize(payload, [ {username: tester, password: 123456}, {username: tester2, password: password}, ]) def test_login_payload(client, payload): resp client.post(/post, jsonpayload) assert resp.status_code 200第三种用法是给用例起别名pytest.mark.parametrize(username,password, [ (tester, 123456), (tester2, password), ], ids[normal_user, second_user]) def test_login_ids(client, username, password): passids只影响测试报告中的用例名不影响执行逻辑。数据量大时建议使用方便在报告中定位失败数据。4.2 用 YAML 文件管理接口用例数据参数写在函数上仍然不方便维护尤其是用例数据多、需要和环境一起变更时。推荐把数据放到 YAML 文件。创建data/login_cases.yamlcases: - name: 登录成功 path: /post payload: username: tester password: 123456 expected: status_code: 200 - name: 用户名为空 path: /post payload: username: password: 123456 expected: status_code: 200创建utils/yaml_loader.pyfrom pathlib import Path import yaml def load_cases(file_name): data_path Path(__file__).parent.parent / data / file_name with open(data_path, r, encodingutf-8) as f: data yaml.safe_load(f) return data[cases]用例文件改造import pytest from utils.yaml_loader import load_cases pytest.mark.parametrize(case, load_cases(login_cases.yaml)) def test_login(client, case): resp client.post(case[path], jsoncase[payload]) assert resp.status_code case[expected][status_code]这里有个容易忽略的坑parametrize在收集阶段就会执行load_cases(login_cases.yaml)。如果 YAML 文件路径写错整个测试模块会收集失败而不是执行失败。所以数据文件路径一定要和项目根目录对应建议在conftest.py里把项目根目录注入sys.path避免不同运行目录导致的导入问题。4.3 conftest.py 是公共 fixture 的注册中心conftest.py可以被同目录及子目录下的测试用例自动加载。公共 fixture 放这里不需要每个测试文件 import。扩展后的testcases/conftest.pyimport pytest from core.api_client import ApiClient from utils.logger import setup_logger pytest.fixture(scopesession) def client(): setup_logger() c ApiClient(test) yield c c.close()如果配置支持--env参数可以用pytest_addoption处理def pytest_addoption(parser): parser.addoption(--env, actionstore, defaulttest, helptest or prod) pytest.fixture(scopesession) def env(request): return request.config.getoption(--env) pytest.fixture(scopesession) def client(env): c ApiClient(env) yield c c.close()这里client依赖envPytest 会自动先准备env。命令行运行时pytest --env test pytest --env prod注意client是session作用域整个测试会话只创建一次。如果某条用例修改了共享 Session 的 headers后续用例会受到影响。这是框架设计时就要考虑的问题。4.4 响应断言不要对着文本字符串做精确匹配接口断言最常见的问题是assert username in resp.text这种写法非常脆弱。响应体可能因为字段顺序、空格、换行、转义字符变化导致误判而且 JSON 里的值即使错误只要字符串片段存在也会通过。推荐对 JSON 字段断言def test_login_json(client): resp client.post(/post, json{username: tester, password: 123456}) data resp.json() assert data[json][username] tester更稳妥的做法是封装统一断言函数def assert_json_field(resp, field_path, expected): data resp.json() current data for part in field_path.split(.): current current[part] assert current expected, f{field_path} expected {expected}, got {current}使用assert_json_field(resp, json.username, tester)这里要注意 YAML 数据文件的类型问题。YAML 中true会被解析为布尔值001会被解析为数字1如果接口期望的是字符串001必须在 YAML 中加引号payload: phone: 001否则断言会莫名其妙失败而且不容易察觉。5. 日志、测试报告与失败重试让接口自动化变成可观测的工程接口自动化用例一旦跑起来最怕的问题就是“为什么失败”。没有日志没有报告没有重试策略排查只能靠猜。这一章解决可观测性。5.1 logging 配合 Requests 记录请求和响应创建utils/logger.pyimport logging import sys def setup_logger(nameapi, levellogging.INFO): logger logging.getLogger(name) if not logger.handlers: handler logging.StreamHandler(sys.stdout) fmt logging.Formatter(%(asctime)s %(levelname)s %(name)s %(message)s) handler.setFormatter(fmt) logger.addHandler(handler) logger.setLevel(level) return logger在conftest.py中调用一次from utils.logger import setup_logger setup_logger()日志会打印到控制台同时可以配置 pytest 的日志输出[pytest] log_cli true log_cli_level INFO这里注意不要重复添加 handler。日志对象是全局的如果每个用例都调用setup_logger并且不加判断会产生重复日志。上面代码中if not logger.handlers就是防止重复。5.2 HTML 报告和 Allure 报告的接入方式在pytest.ini中配置 pytest-html[pytest] addopts -q --htmlreport.html --self-contained-html testpaths testcases--self-contained-html会把 CSS 和 JS 合并到单个 HTML 文件方便发送和归档。运行后生成report.html直接用浏览器打开。如果团队要更丰富的报告可以接入 Allurepip install allure-pytest运行时pytest --alluredirallure-results然后用 Allure 命令行生成报告allure serve allure-resultsAllure 的优势是按测试套件、用例、步骤展示结果适合长期存档和团队协作。但搭建成本比 pytest-html 高需要安装 Java 运行环境和 allure 命令行工具。学习环境建议先用 pytest-html有精力再迁移 Allure。5.3 失败重试什么时候该重试什么时候不该接口自动化最常见的偶发失败原因是网络抖动、测试环境服务重启、依赖接口暂时不可用。这时可以使用 pytest-rerunfailures。在pytest.ini配置addopts -q --reruns 2 --reruns-delay 1或者在用例上单独标记import pytest pytest.mark.flaky(reruns2, reruns_delay1) def test_login(client): resp client.post(/post, json{username: tester, password: 123456}) assert resp.status_code 200重试必须“受控”。建议遵守三条规则重试次数限制在 2 到 3 次不要无限重试。重试延迟至少 1 秒避免请求风暴。断言失败、业务逻辑错误、鉴权失败不要重试重试只会掩盖真实问题。如果配置了全局--reruns建议针对关键用例使用--strict-markers和指定 mark避免所有用例都被无差别重试。5.4 429 too many requests 的工程化处理在真实接口测试中经常看到类似日志exceeded retry limit, last status: 429 too many requests这个报错表示请求频率超过服务端限制。429 是 HTTP 标准状态码服务端通过它告诉调用方请放慢速度。遇到 429第一反应不应该是“把这个报错隐藏掉”而是按以下顺序处理检查是否有循环请求或用例并发过高。如果使用了 pytest-xdist 的-n参数先把并发数调小。增加退避重试。pytest-rerunfailures 支持延迟重试pytest --reruns 3 --reruns-delay 5如果服务端返回了Retry-After响应头要优先遵守这个时间。Requests 可以通过resp.headers.get(Retry-After)读取然后等待对应秒数再继续。检查是否发送了重复请求或者存在未结束的上一次任务。限流往往是短时间内请求数骤增导致的先处理代码逻辑问题再考虑重试。这里尤其要注意不要把重试写成无限循环也不要通过绕过频率检测的方式强行请求。合规的做法是接受服务端限流策略调整测试节奏。如果测试必须高频运行应该和接口提供方确认测试环境和额度而不是在脚本层硬扛。6. 常见报错排查从现象倒推根因接口自动化框架跑起来之后报错会集中出现在几个位置。这一章按“现象 → 可能原因 → 检查方式 → 解决方案”的方式整理。6.1 fixture 作用域不对导致用例互相污染现象单独执行某条用例通过放在整个测试套件里执行时失败尤其是登录态相关用例。原因client是session作用域Session 中的 headers、Cookie 是共享状态。前一条用例修改了 headers后一条用例带着被修改后的状态访问接口。检查方式在失败用例前打印client.session.headers对比单独执行时的差异。解决方案不要在用例中直接修改 session 级共享对象。如果某条用例需要特殊请求头可以在请求方法中临时传入headers而不是修改client.session.headers。Pytest fixture 作用域速查作用域生命周期适用场景function每个用例执行一次临时数据、独立数据准备class每个测试类执行一次类级别的准备工作module每个模块执行一次模块内共享资源session整个测试会话执行一次连接、登录态、全局配置6.2 连接超时、SSL 报错和地址配置错误现象requests.exceptions.ConnectionError: Max retries exceeded with url: /post requests.exceptions.SSLError: CERTIFICATE_VERIFY_FAILED requests.exceptions.ConnectTimeout排查顺序先用curl检查目标地址是否能通curl -i https://httpbin.org/post如果 curl 也失败说明是网络或服务问题不是框架问题。检查 URL 拼接是否正确。封装里如果base_url末尾带了/而请求路径又以/开头会得到/api//post这类地址。建议统一约定base_url不以/结尾请求路径以/开头。检查超时配置。学习环境可以设置 10 秒生产环境的读接口建议 5 秒写接口建议 10 到 15 秒。超时时间太短会误报失败太长会拖慢整体执行。遇到CERTIFICATE_VERIFY_FAILED不要直接写verifyFalse逃避。优先确认系统时间、CA 证书、服务端证书链是否正常。测试环境如果确实需要临时忽略证书校验要在代码中显式注释并只在测试环境使用。6.3 断言失败后按什么顺序排查断言失败不一定代表接口有 bug也可能请求参数没传对、测试数据过期、环境切换后字段变了。建议按这个顺序查状态码是否符合预期。如果状态码是 500优先看服务端日志而不是继续看响应体。响应体是否是完整 JSON。某些错误页面返回 HTMLresp.json()会抛异常。请求参数是否真的发送了。打开日志确认params、json、headers字段内容。是否环境问题。同一套用例在 test 环境通过、在 prod 环境失败说明数据、接口契约或网络策略存在差异。是否是断言表达式问题。例如assert username in resp.text这种弱断言容易通过也容易误判。排查时不要只看最后一行报错。接口自动化最有效的信息是“请求参数 响应体 日志时间点”所以框架中日志越详细排查越快。6.4 切换环境后用例大面积失败现象本地--env test运行正常CI 里--env prod或--env staging运行大量失败。可能原因config.yaml中没有对应环境节点。新环境的域名、端口、鉴权方式不同。测试数据中的数据只存在于 test 环境prod 环境没有对应数据。环境变量没有正确传入框架仍然在读取默认环境。解决方案配置读取统一走Config(env)不要在用例里用if env prod写很多分支。环境差异数据按环境拆分文件例如data/test/login_cases.yaml和data/prod/login_cases.yaml。CI 脚本中显式传环境变量export API_ENVtest pytest --env test环境切换失败是框架设计问题不是用例问题。出现大面积失败时先检查配置文件和环境变量再查看具体接口报错最后才看断言逻辑。7. 生产环境落地最佳实践、检查清单与扩展方向框架在本地跑通只是开始真正价值体现在团队协作、CI 回归和生产巡检。最后的落地方案直接决定框架能维持多久。7.1 学习环境、测试环境与生产环境的差异同一个框架在不同阶段要做不同取舍维度学习环境测试环境生产环境目标快速跑通回归和联调冒烟和监控数据公开接口或本地 mock脱敏测试数据专用账号或只读数据报告本地 HTMLCI 归档自动归档并通知重试可以不配按场景配置必须受控安全随意脱敏敏感信息不回显、不打日志日志控制台即可文件 CI 日志日志平台和告警生产环境的接口自动化还要额外考虑请求不能对业务数据造成污染。调用频率必须控制在接口方允许范围。错误需要告警而不能只停留在测试报告里。要有回滚机制压测或长时间回归不要直接在生产环境执行。7.2 接口自动化框架上线前的检查清单上线前可以逐项核对依赖版本已经固定requirements.txt可以重复安装。base_url不在用例代码里写死全部走配置。测试数据独立维护不依赖用例执行顺序。fixture 作用域清晰没有用例之间共享状态污染。日志能记录请求方法、URL、状态码、响应摘要但不打印完整 token 和密码。断言不依赖resp.text字符串包含按 JSON 字段断言。超时时间已经配置没有请求被无限挂起。失败重试次数受控不会掩盖真实失败。报告可在 CI 中生成并归档。敏感信息不进入 Git配置文件有示例模板。这些条目看起来简单但实际项目里每一条都能对应一个真实故障。例如不打印 token是因为日志一旦上传到日志平台泄露风险就会放大。断言不依赖文本是因为响应体字段顺序调整会导致大量误报。7.3 后续可扩展的方向框架稳定后可以从以下方向继续扩展接入 CI/CD。以 GitLab CI 为例可以在.gitlab-ci.yml中增加测试任务stages: - test api-test: stage: test script: - pip install -r requirements.txt - pytest --env test artifacts: paths: - report.html登录态管理。用 fixture 统一获取 token写入 Session headers而不是每条用例各自登录。数据隔离。每个环境使用独立测试账号、独立数据文件避免环境间数据互相影响。契约校验。在断言之上引入 JSON Schema 或 Schemathesis校验响应结构是否稳定。性能回归。接口功能框架不要混入压测逻辑可以结合 Locust 单独建设性能测试工程功能回归和性能回归使用不同的数据规模和频率。如果只记一条那就是用例层只写业务表达把请求发送、配置读取、日志采集和重试策略都下沉到框架层。这样新人接手时不需要关心 HTTP 细节接口变更时也只需要改数据文件和少量断言。下一步可以从把第一条登录用例跑通开始再把报告和 CI 接上框架的价值会在每次回归时体现出来。