ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零搭建Pytest+Requests接口自动化测试框架

从零搭建Pytest+Requests接口自动化测试框架 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 RequestsUnittest 也能用但用例编写比 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-htmlpip install pytest-html如果希望失败用例自动重跑可以安装 pytest-rerunfailurespip 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_urlBASE_URL, headersNone): 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, jsonpayload) return resp这样在每个测试用例里调用LoginApi().login()就完成了对接口的访问。如果接口返回结构复杂也可以把响应解析成 JSON 后再断言。5. Pytest 核心用法从编写用例到 Fixture5.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在项目根目录执行pytestPytest 会自动搜索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(scopesession) 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, jsonpayload) 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, jsonpayload) 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, encodingutf-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, jsonpayload) assert resp.status_code login_info[expected_status]这样新增测试数据时只需要修改 YAML 文件不需要改代码。6. 完整实战从零搭建一个可运行的接口测试框架现在我们把前面所有知识点整合起来搭建一个真正完整的项目。6.1 requirements.txt在项目根目录创建requirements.txtpytest8.2.0 requests2.32.3 pytest-html4.1.0 pytest-rerunfailures14.0 PyYAML6.0.1版本可以根据实际情况调整。安装依赖pip install -r requirements.txt6.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 --htmlreports/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(scopesession) def request_client(): client BaseRequest() yield client client.session.close() pytest.fixture(scopesession) def login_token(request_client): 获取登录后的 token实际接口返回结构按项目调整 这里只演示思路具体字段必须和真实接口一致 payload {username: admin, password: 123456} resp request_client.post(/post, jsonpayload) 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: fBearer {login_token}} resp request_client.get(/get, headersheaders) assert resp.status_code 2006.4 编写业务用例下面以最常见的用户管理模块为例演示一套完整的测试用例写法。创建testcases/test_user.py# 文件路径testcases/test_user.py import pytest from common.base_request import BaseRequest pytest.fixture(scopemodule) 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, jsonpayload) 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, jsonpayload) assert resp.status_code 200 def test_delete_user(user_api): payload {id: 1} resp user_api.delete(/delete, jsonpayload) 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, ftest_{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, encodingutf-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中使用这个 loggerfrom 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 编码读取文件时指定encodingutf-8HTML 报告没有样式未使用--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, actionstore, defaultdev, help指定测试环境) pytest.fixture(scopesession) def env(request): return request.config.getoption(--env)运行时pytest --env qa然后在代码中根据环境读取对应配置。9.3 日志与报告日志信息要包含请求方法、URL、请求参数、响应状态码。这样出现问题时可以快速定位。不要记录敏感信息比如密码明文、token 等。如果必须打印可以脱敏。HTML 报告在团队内可以用 Jenkins 等工具归档方便查看历史趋势。如果用例数量多建议开启失败重跑减少因网络波动导致的误报。9.4 异常处理和边界条件从工程角度来说测试代码也是代码同样要有健壮性思维。比如请求接口时捕获异常避免某一条用例网络异常导致整个测试中断try: resp client.post(/post, jsonpayload) except requests.exceptions.Timeout: pytest.fail(请求超时) except requests.exceptions.RequestException as e: pytest.fail(f请求异常: {e})虽然 Pytest 也能捕获未处理异常但显式处理可以让失败信息更有指导意义。9.5 与 CI/CD 集成框架稳定后建议与 Jenkins 或 GitLab CI 集成。常见的步骤是拉取代码。创建虚拟环境并安装依赖。执行 pytest 命令。发布测试报告。将失败结果通知到钉钉、企微或邮件。这样接口自动化测试才能从“本地手动跑一跑”升级为团队日常质量保障的一部分。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然后慢慢完善它比收藏一堆资料有用得多。
返回列表