从零搭建Pytest+Requests接口自动化测试框架
2026/9/6 10:01:04 网站建设 项目流程

1. 接口自动化测试框架为什么要自己搭?

做过接口测试的同学应该都有体会:Postman 调试接口确实方便,但一旦用例数量超过几百条,或者需要接入 CI 流水线、每天定时执行、生成测试报告,再去手工点 Postman 就会非常痛苦。

我之前在一个中后台项目里负责接口回归测试,业务方每周发两次版本,每次回归要测 300 多条接口用例。最开始团队用 Postman 里的 Collection Runner 跑,但有几个问题始终绕不开:

  • 用例执行顺序不可控,依赖登录 token 的接口经常报 401。
  • 断言写在 Postman 的脚本里,不好维护,也没法复用。
  • 测试报告不直观,出了问题还要自己去翻响应日志。
  • 没法方便地对接 Jenkins,命令行执行不友好。

后来我把整套回归流程迁移到 Pytest + Requests 搭建的接口自动化测试框架上,用例管理、数据驱动、断言、报告生成、失败重跑这些问题都得到了解决。这篇文章就是基于那段时间的落地经验整理出来的,非常适合准备从工具型测试转向代码型测试的读者。

本文会从一个完全空白的目录开始,一步一步搭建一个可运行的 Pytest + Requests 接口自动化测试框架。你不需要有非常深厚的 Python 基础,只要会写简单的函数、能看懂字典和列表,就可以跟下来。

2. 前置知识:Pytest 和 Requests 分别是干什么的

2.1 Requests:让 Python 发 HTTP 请求变得简单

Requests 是 Python 生态里最常用的 HTTP 客户端库。它的底层封装了 urllib,但使用体验比 urllib 友好太多。比如发送一个 GET 请求,用 Requests 只需要这样:

import requests resp = requests.get("https://httpbin.org/get") print(resp.status_code) print(resp.json())

它支持 GET、POST、PUT、DELETE 等常见方法,也支持 headers、cookies、params、json、files 等参数。在接口自动化测试里,我们主要用到它的这几个能力:

  • 发送各种 HTTP 方法请求。
  • 自定义请求头、请求体。
  • 处理响应状态码、响应头、响应体。
  • 保持会话(Session),自动管理 cookies。
  • 超时控制、代理设置、SSL 校验开关。

在接口测试中,一般用requests.Session()而不是直接调用requests.get(),因为 Session 可以维持同一个连接池,并且自动保存 cookies。这对需要登录态的接口非常重要。

2.2 Pytest:强大的 Python 测试框架

Pytest 是 Python 目前最主流的测试框架。它的名字看起来很朴素,但功能非常丰富:

  • 自动发现测试用例,不需要手动写 suite。
  • 使用assert断言,直观不绕弯。
  • 支持fixture,可以在测试前做初始化、后置清理。
  • 支持参数化,一份代码跑多组数据。
  • 支持插件扩展,比如生成 HTML 报告、失败重跑、并发执行。

对于接口自动化测试来说,Pytest 的价值在于组织用例和执行控制。把 Requests 的请求逻辑封装成函数或类,再用 Pytest 编写测试用例,就能得到一个结构清晰、可持续维护的接口测试工程。

2.3 为什么是 Pytest + Requests 而不是其他组合

市面上常见的接口自动化方案还有:

  • Postman + Newman:适合轻量级、少量用例,但断言和逻辑复杂时维护成本高。
  • Java + RestAssured + TestNG:适合 Java 技术栈团队,但写起来比 Python 冗余。
  • Python + Unittest + Requests:Unittest 也能用,但用例编写比 Pytest 繁琐,断言方式也没有 Pytest 简洁。
  • RobotFramework + RequestsLibrary:关键字驱动,学习成本低,但灵活性受限。

Pytest + Requests 的组合胜在简单、自由、社区资源多。Pytest 本身非常轻量,Requests 也足够强大,两者配合可以快速搭建一个真正适合自己项目的测试框架。

3. 环境准备与项目初始化

开始之前,先确认一下你的环境。

3.1 安装 Python 和 pip

本文示例使用的 Python 版本为 3.8 及以上,建议使用 3.9 或更高版本。在终端执行:

python --version pip --version

如果没有安装 Python,可以去官网下载安装包。安装时记得勾选“Add Python to PATH”。

3.2 安装 Pytest 和 Requests

使用 pip 安装:

pip install pytest requests

如果你希望生成漂亮的 HTML 测试报告,可以额外安装 pytest-html:

pip install pytest-html

如果希望失败用例自动重跑,可以安装 pytest-rerunfailures:

pip install pytest-rerunfailures

还可以安装 pytest-xdist 来并行执行用例:

pip install pytest-xdist

安装完成后,可以用一个简单命令验证:

pytest --version

输出类似:

pytest 8.2.0

说明安装成功。

3.3 创建项目目录结构

不要把所有代码堆在一个文件里。一个清晰的项目结构,能让框架的维护成本大幅降低。下面是我推荐的最小项目结构:

api_test_framework/ ├── config/ # 配置文件 │ ├── __init__.py │ └── settings.py # 环境地址、超时时间等 ├── common/ # 公共封装 │ ├── __init__.py │ ├── base_request.py # Requests 会话封装 │ ├── read_yaml.py # YAML 文件读取,如果使用 YAML 管理数据 │ └── read_excel.py # Excel 数据读取,如需要 ├── data/ # 测试数据 │ └── login_data.yaml ├── testcases/ # 测试用例 │ ├── __init__.py │ └── test_login.py ├── reports/ # 测试报告目录 ├── logs/ # 日志目录 ├── conftest.py # Pytest fixture 定义 ├── pytest.ini # Pytest 配置 └── requirements.txt # 依赖清单

实际项目中,可以根据团队习惯调整。但“配置、公共方法、测试数据、测试用例”分离的原则不要变。

4. 核心封装:把 Requests 变成好用的测试工具

直接在每个用例里写requests.get()当然可以,但会造成大量重复代码。更好的做法是封装一个BaseRequest类,统一处理请求发送和响应解析。

4.1 配置文件

先创建config/settings.py,用来存放环境信息和公共参数:

# 文件路径:config/settings.py # 接口环境地址,实际项目可切换 BASE_URL = "https://httpbin.org" # 全局请求头 HEADERS = { "Content-Type": "application/json", "User-Agent": "api-test-framework" } # 请求超时时间(秒) TIMEOUT = 10 # 是否开启 SSL 校验,测试环境常用 False VERIFY = False

如果你的项目需要支持多套环境,可以改成读取环境变量的方式,或者用 YAML 存放环境配置。这里先保持简单。

4.2 封装请求类

common/base_request.py中封装一个通用的请求类:

# 文件路径:common/base_request.py import requests import logging from config.settings import BASE_URL, HEADERS, TIMEOUT, VERIFY logger = logging.getLogger(__name__) class BaseRequest: def __init__(self, base_url=BASE_URL, headers=None): self.base_url = base_url self.session = requests.Session() if headers: self.session.headers.update(headers) else: self.session.headers.update(HEADERS) def request(self, method, url, **kwargs): """ 统一的请求入口 :param method: 请求方法,GET/POST/PUT/DELETE等 :param url: 路径,如 /get :param kwargs: 其余请求参数,如 params, json, headers, timeout :return: Response 对象 """ full_url = self.base_url + url if url.startswith('/') else self.base_url + '/' + url # 如果没有单独传入 timeout,则使用配置中的默认超时 kwargs.setdefault("timeout", TIMEOUT) kwargs.setdefault("verify", VERIFY) logger.info(f"发起请求: {method} {full_url}") logger.info(f"请求参数: {kwargs}") resp = self.session.request(method, full_url, **kwargs) logger.info(f"响应状态码: {resp.status_code}") return resp def get(self, url, **kwargs): return self.request("GET", url, **kwargs) def post(self, url, **kwargs): return self.request("POST", url, **kwargs) def put(self, url, **kwargs): return self.request("PUT", url, **kwargs) def delete(self, url, **kwargs): return self.request("DELETE", url, **kwargs)

这样封装后,后续写接口用例时,只需要创建 BaseRequest 实例,调用post()等方法即可。

4.3 接口操作类

在实际项目中,我们经常把每个接口模块封装成一个类。比如登录接口:

# 文件路径:common/api_login.py from common.base_request import BaseRequest class LoginApi: def __init__(self): self.request = BaseRequest() def login(self, username, password): """ 登录接口 示例:POST /post 仅用于演示,实际项目请替换为自己的登录接口 """ payload = { "username": username, "password": password } resp = self.request.post("/post", json=payload) return resp

这样在每个测试用例里,调用LoginApi().login()就完成了对接口的访问。如果接口返回结构复杂,也可以把响应解析成 JSON 后再断言。

5. Pytest 核心用法:从编写用例到 Fixture

5.1 编写第一个测试用例

testcases目录下创建test_login.py

# 文件路径:testcases/test_login.py import pytest from common.api_login import LoginApi class TestLogin: def test_login_success(self): api = LoginApi() resp = api.login("admin", "123456") assert resp.status_code == 200 data = resp.json() # 这里只是演示,实际断言字段需要根据接口文档调整 assert "args" in data

在项目根目录执行:

pytest

Pytest 会自动搜索test_*.py*_test.py文件,并执行其中test_开头的函数或类方法。

5.2 使用 fixture 完成初始化和数据清理

fixture 是 Pytest 最强大的功能之一。它可以在测试执行前后做事情,并且可以在多个用例之间共享。

我们把 BaseRequest 实例定义为一个会话级 fixture,避免每个用例都重复创建 Session:

# 文件路径:conftest.py import pytest from common.base_request import BaseRequest @pytest.fixture(scope="session") def request_client(): """ 返回一个 BaseRequest 实例,整个测试会话只会创建一次 """ client = BaseRequest() yield client # 测试结束后关闭会话 client.session.close()

然后在用例中直接使用这个 fixture:

# 文件路径:testcases/test_login.py def test_login_success(request_client): payload = { "username": "admin", "password": "123456" } resp = request_client.post("/post", json=payload) assert resp.status_code == 200

可以看到,有了 fixture 之后,用例代码更简洁了。

5.3 参数化:使用 pytest.mark.parametrize

接口测试最常用的场景是对同一接口用多组数据执行。Pytest 的参数化可以优雅地解决这个问题:

# 文件路径:testcases/test_login.py import pytest @pytest.mark.parametrize("username,password,expected_status", [ ("admin", "123456", 200), ("user1", "wrong", 200), ("", "123456", 200), ]) def test_login_with_params(request_client, username, password, expected_status): payload = { "username": username, "password": password } resp = request_client.post("/post", json=payload) assert resp.status_code == expected_status

执行时,Pytest 会自动生成多条测试用例。上面的示例数据只是一个演示结构,真正项目中expected_status应该根据接口的实际业务逻辑来定。

5.4 数据与用例分离

当用例数据很多时,不建议直接写在代码里。可以将测试数据放到 YAML 文件或 Excel 中。这里以 YAML 为例:

创建data/login_data.yaml

- username: admin password: "123456" expected_status: 200 - username: user1 password: "wrong" expected_status: 200 - username: "" password: "123456" expected_status: 200

common/read_yaml.py中提供一个读取函数:

# 文件路径:common/read_yaml.py import yaml def read_yaml_file(file_path): with open(file_path, "r", encoding="utf-8") as f: return yaml.safe_load(f)

使用数据驱动的方式编写用例:

# 文件路径:testcases/test_login.py import pytest from common.read_yaml import read_yaml_file # 注意:这里要根据你的项目路径调整文件路径 logins = read_yaml_file("data/login_data.yaml") @pytest.mark.parametrize("login_info", logins) def test_login_from_yaml(request_client, login_info): payload = { "username": login_info["username"], "password": login_info["password"] } resp = request_client.post("/post", json=payload) assert resp.status_code == login_info["expected_status"]

这样新增测试数据时,只需要修改 YAML 文件,不需要改代码。

6. 完整实战:从零搭建一个可运行的接口测试框架

现在,我们把前面所有知识点整合起来,搭建一个真正完整的项目。

6.1 requirements.txt

在项目根目录创建requirements.txt

pytest==8.2.0 requests==2.32.3 pytest-html==4.1.0 pytest-rerunfailures==14.0 PyYAML==6.0.1

版本可以根据实际情况调整。安装依赖:

pip install -r requirements.txt

6.2 pytest.ini 配置

Pytest 的一些统配行为可以通过配置文件控制。创建pytest.ini

[pytest] minversion = 7.0 testpaths = testcases python_files = test_*.py python_classes = Test* python_functions = test_* addopts = -v -s --html=reports/report.html --self-contained-html

说明:

  • testpaths:指定测试用例目录。
  • python_files:指定测试文件命名规则。
  • addopts:追加命令行参数,这里合并了 HTML 报告参数。
  • --self-contained-html:让 HTML 报告包含所有资源,单独一个文件即可分享。

6.3 conftest.py 中定义全局 fixture

我们可以在conftest.py中定义多个 fixture,比如登录接口返回 token:

# 文件路径:conftest.py import pytest import requests from common.base_request import BaseRequest @pytest.fixture(scope="session") def request_client(): client = BaseRequest() yield client client.session.close() @pytest.fixture(scope="session") def login_token(request_client): """ 获取登录后的 token,实际接口返回结构按项目调整 这里只演示思路,具体字段必须和真实接口一致 """ payload = {"username": "admin", "password": "123456"} resp = request_client.post("/post", json=payload) data = resp.json() # 假设接口返回 {"token": "xxxxx"} token = data.get("json", {}).get("token", "") return token

在其他用例中,如果需要登录态,直接注入login_token即可:

def test_order_list(request_client, login_token): headers = {"Authorization": f"Bearer {login_token}"} resp = request_client.get("/get", headers=headers) assert resp.status_code == 200

6.4 编写业务用例

下面以最常见的用户管理模块为例,演示一套完整的测试用例写法。

创建testcases/test_user.py

# 文件路径:testcases/test_user.py import pytest from common.base_request import BaseRequest @pytest.fixture(scope="module") def user_api(): api = BaseRequest() yield api api.session.close() # 假设 /post 只是调试接口,这里仅用来演示请求方式 def test_create_user(user_api): payload = {"name": "张三", "age": 20} resp = user_api.post("/post", json=payload) assert resp.status_code == 200 resp_data = resp.json() assert resp_data["json"]["name"] == "张三" def test_get_user_list(user_api): resp = user_api.get("/get") assert resp.status_code == 200 assert "args" in resp.json() def test_update_user(user_api): payload = {"id": 1, "name": "李四"} resp = user_api.put("/put", json=payload) assert resp.status_code == 200 def test_delete_user(user_api): payload = {"id": 1} resp = user_api.delete("/delete", json=payload) assert resp.status_code == 200

强调一下:上面示例中的/post/get是 httpbin 调试接口,实际项目中请替换为你们后端真实的接口地址,并且断言字段也要与真实返回结构一致。

6.5 运行测试并查看报告

在项目根目录执行:

pytest

如果你希望只跑某个文件:

pytest testcases/test_login.py

如果你希望失败用例自动重试 2 次:

pytest --reruns 2

如果你还安装了 pytest-xdist,可以并行执行:

pytest -n 3

执行完成后,会产生reports/report.html。使用浏览器打开该文件即可看到 HTML 测试报告,里面包含了用例执行状态、耗时、失败原因等信息。

7. 进阶功能:日志、断言封装与动态请求参数

7.1 添加日志输出

好的日志能极大提升排错效率。我们可以在common/logger.py中创建统一日志工具:

# 文件路径:common/logger.py import logging import os import time def setup_logger(): log_dir = "logs" if not os.path.exists(log_dir): os.makedirs(log_dir) log_file = os.path.join(log_dir, f"test_{time.strftime('%Y%m%d_%H%M%S')}.log") logger = logging.getLogger("api_test") logger.setLevel(logging.INFO) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler = logging.FileHandler(log_file, encoding="utf-8") file_handler.setFormatter(formatter) console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) logger.addHandler(file_handler) logger.addHandler(console_handler) return logger logger = setup_logger()

BaseRequest中使用这个 logger:

from common.logger import logger

这样每次请求都会记录到日志文件中,方便后续定位问题。

7.2 封装断言工具

Pytest 的assert虽然简单,但在接口测试中我们经常需要断言 JSON 字段、状态码、响应时间等。可以封装一个断言类:

# 文件路径:common/assert_tool.py import json class AssertTool: @staticmethod def status_code(resp, expected): assert resp.status_code == expected, f"状态码不一致,实际: {resp.status_code}, 期望: {expected}" @staticmethod def json_field(resp, field, expected): data = resp.json() assert data.get(field) == expected, f"字段 {field} 不一致,实际: {data.get(field)}, 期望: {expected}" @staticmethod def json_contains(resp, key): data = resp.json() assert key in data, f"响应中不存在字段 {key}"

使用位置:

from common.assert_tool import AssertTool def test_example(request_client): resp = request_client.get("/get") AssertTool.status_code(resp, 200) AssertTool.json_contains(resp, "args")

这样做的好处是:断言逻辑统一,以后如果要增加“响应时间不能超过 3 秒”等全局断言,只需要修改工具类。

8. 常见问题与排查思路

在搭建和使用框架的过程中,大家经常会遇到下面几类问题。这里整理了一个排查表,按顺序检查通常能快速定位。

问题现象常见原因解决思路
运行pytest提示找不到模块项目根目录未加入 Python 路径在项目根目录运行 pytest,或检查 conftest.py 位置
请求超时接口响应慢或网络不通增加 timeout 配置,检查目标服务状态,测试时使用 mock
请求返回 500后端报错或参数缺少查看日志,用 Postman 或 curl 手工发一次对比
接口返回 401/403token 失效或权限不足检查 login_token fixture 是否正常,查看日志中的 token 是否过期
执行全部用例时相互影响用例依赖了某个接口的状态使用 fixture 隔离初始化,用例尽量独立
接口请求被限流,报 429 Too Many Requests单位时间内请求过于频繁降低并发数,增加请求间隔,或优化用例减少重复请求
中文乱码编码格式不一致统一使用 UTF-8 编码,读取文件时指定encoding="utf-8"
HTML 报告没有样式未使用--self-contained-html在 pytest.ini 中加入该参数,或者重新生成报告

8.1 关于 429 限流问题

最近在很多技术群里看到大家讨论一个问题:请求发送太频繁时,接口返回429 Too Many Requests。这在接口自动化测试中也非常常见。

HTTP 429 表示“在一定时间内发送了太多请求”,服务端启用了限流策略。遇到这种情况,尽量不要盲目加大并发。可以这样处理:

  • 优先排查是否有用例在循环中高频请求同一接口。
  • 在请求之间增加 sleep 间隔,比如time.sleep(0.1)
  • 如果是 Pytest 并行执行导致的限流,降低-n的数值,或者去掉并行。
  • 把相关用例标记为串行执行,或者分批次运行。
  • 确认是否需要携带认证信息,部分服务对未认证请求限流更严格。

在框架设计时,可以在BaseRequest中增加一个可配置的请求间隔:

import time # 在请求方法内,可选增加间隔 def set_request_delay(self, seconds): time.sleep(seconds)

在需要时调用即可。虽然这不是一个优雅的解决方案,但在面对限流场景时很实用。

9. 最佳实践与工程建议

接口自动化测试框架搭建起来很简单,但真正在团队中落地并持续稳定运行,还需要注意以下几点。

9.1 用例设计原则

  • 用例之间尽量不要有依赖。每一个测试用例都应该可以独立执行。
  • 如果需要依赖登录态,使用 fixture 统一管理,不要在每个用例内部手动登录。
  • 数据尽量使用独立的测试数据,避免影响其他用例。
  • 断言要明确,不要只断言状态码,关键业务字段也要验证。
  • 对异常场景要有覆盖,比如参数缺省、参数类型错误、无权限访问等。

9.2 配置管理

不要把所有环境的接口地址硬编码在代码里。推荐使用环境变量或配置文件区分 dev、qa、prod 环境。

可以使用pytest_addoption支持命令行传入环境参数:

# conftest.py 中增加 def pytest_addoption(parser): parser.addoption("--env", action="store", default="dev", help="指定测试环境") @pytest.fixture(scope="session") def env(request): return request.config.getoption("--env")

运行时:

pytest --env qa

然后在代码中根据环境读取对应配置。

9.3 日志与报告

  • 日志信息要包含请求方法、URL、请求参数、响应状态码。这样出现问题时可以快速定位。
  • 不要记录敏感信息,比如密码明文、token 等。如果必须打印,可以脱敏。
  • HTML 报告在团队内可以用 Jenkins 等工具归档,方便查看历史趋势。
  • 如果用例数量多,建议开启失败重跑,减少因网络波动导致的误报。

9.4 异常处理和边界条件

从工程角度来说,测试代码也是代码,同样要有健壮性思维。

比如请求接口时捕获异常,避免某一条用例网络异常导致整个测试中断:

try: resp = client.post("/post", json=payload) except requests.exceptions.Timeout: pytest.fail("请求超时") except requests.exceptions.RequestException as e: pytest.fail(f"请求异常: {e}")

虽然 Pytest 也能捕获未处理异常,但显式处理可以让失败信息更有指导意义。

9.5 与 CI/CD 集成

框架稳定后,建议与 Jenkins 或 GitLab CI 集成。常见的步骤是:

  1. 拉取代码。
  2. 创建虚拟环境并安装依赖。
  3. 执行 pytest 命令。
  4. 发布测试报告。
  5. 将失败结果通知到钉钉、企微或邮件。

这样,接口自动化测试才能从“本地手动跑一跑”升级为团队日常质量保障的一部分。

9.6 保持框架简单

最后一条建议是:框架能解决问题就好,不要过度设计。有些项目刚起步就引入几十个插件、封装多层类,最后维护成本比手工测试还高。建议先实现最小可用框架,再根据实际需要逐步增加功能。

10. 总结与下一步学习方向

到这里,我们已经从零搭建了一个基于 Pytest + Requests 的接口自动化测试框架。回顾一下,主要内容包括:

  • Requests 基础用法和 Session 管理。
  • BaseRequest 封装,统一处理 URL、超时、日志。
  • Pytest fixture 实现前置初始化和会话复用。
  • 参数化与数据驱动,让测试数据从代码中分离。
  • 整体项目结构的搭建与运行。
  • 常见问题(如 429 限流)的排查思路。
  • 工程实践中的配置管理、日志与 CI 集成建议。

如果你已经能独立写出这样的框架,下一步可以往这几个方向继续深入:

  • 学习 Pytest 插件开发,自定义符合团队需求的报告或执行逻辑。
  • 引入 Allure 测试报告,展示更丰富的测试结果和历史趋势。
  • 学习如何用 Docker 运行测试,让接口自动化测试在 CI 环境中稳定复现。
  • 深入理解 HTTP 协议,掌握更复杂的认证机制,比如 OAuth2.0、JWT、加签验签。
  • 如果团队项目是 Java 技术栈,也可以了解 Java 侧的 RestAssured + TestNG 方案,思想是相通的。

接口自动化只是质量保障中的一环,它不能解决所有问题,但确实能将重复性的回归工作自动化。后续实践中如果遇到新问题,建议多去看官方文档和源码,那是最好的学习材料。希望这篇教程能帮你少踩一些自己曾经踩过的坑。动手搭建一个最简单的 demo,然后慢慢完善它,比收藏一堆资料有用得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询