Python接口自动化测试入门:pytest与requests实战指南

发布时间:2026/7/2 23:56:52
Python接口自动化测试入门:pytest与requests实战指南 1. 项目概述为什么选择 pytest requests 做接口测试如果你刚开始接触接口自动化测试面对 Postman、JMeter、Apifox 等一堆工具可能会有点懵。工具虽好但当你需要把测试集成到 CI/CD 流水线或者想对测试用例进行更精细的管理和数据驱动时代码化的方案往往更灵活、更强大。这就是pytest和requests这对黄金组合的价值所在。requests库让发送 HTTP 请求变得像说话一样简单而pytest则提供了一个极其强大且优雅的框架来组织、运行和报告你的测试。它们不是要替代 Postman 这类工具而是为你打开了另一扇门让你能用程序员的方式更系统、更自动化地解决接口测试问题。我见过很多团队初期用 Postman 点点点后期用例多了维护起来头疼回归测试效率低下。这时转向pytest requests不仅能实现用例的代码化管理、数据与逻辑分离还能轻松生成 Allure 等漂亮的测试报告与 Jenkins 等集成工具无缝对接。更重要的是这套组合的学习曲线相对平缓只要你有点 Python 基础就能快速上手产出价值。接下来我就以一个最基础的入门示例为引子带你拆解其中的每一个环节并分享那些官方文档里不会写的“踩坑”经验。2. 环境准备与核心工具解析2.1 安装与配置一步到位的基础搭建万事开头难但环境的搭建可以很简单。首先确保你安装了 Python建议 3.7 及以上版本。然后打开你的命令行终端使用 pip 进行安装。这里有个小技巧为了依赖清晰我强烈建议为每个项目创建独立的虚拟环境。# 创建项目目录并进入 mkdir api-test-demo cd api-test-demo # 创建虚拟环境以 venv 为例 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 安装核心库 pip install pytest requests安装完成后可以通过pytest --version和python -c “import requests; print(requests.__version__)”来验证安装是否成功。pytest和requests的版本兼容性通常很好但如果你遇到一些奇怪的问题可以尝试指定稍旧一点的稳定版本例如pip install pytest7.4.4 requests2.31.0。注意虚拟环境是 Python 项目管理的“最佳实践”。它能把项目的依赖隔离起来避免不同项目间因库版本冲突导致“它在我电脑上能跑”的尴尬局面。项目根目录下的requirements.txt文件可通过pip freeze requirements.txt生成记录了所有依赖方便团队其他成员或部署环境快速复现。2.2 理解核心工具pytest 与 requests 的角色在开始写代码前我们需要厘清这两个库各自承担什么责任这样写用例时思路才会清晰。requests库你的“网络信使”它的核心功能就是模拟浏览器或客户端向服务器发送 HTTP 请求并接收响应。我们用它来完成接口测试中最本质的动作调用接口。它的 API 设计非常人性化几乎是对 HTTP 协议的直译requests.get(url)发送 GET 请求。requests.post(url, data...)发送 POST 请求。其他如put()delete()patch()等对应其他 HTTP 方法。你可以方便地添加请求头headers、传递参数params、提交表单或 JSON 数据data或json以及处理 Cookies 和会话。在测试中我们通过requests发送请求然后检查返回的响应对象response的状态码response.status_code、响应体response.text或response.json()、响应头等信息来判断接口行为是否符合预期。pytest框架你的“测试管家”如果说requests是干具体活的那pytest就是负责调度、管理和汇报的管家。它提供了一套编写测试用例的规则和丰富的功能来支撑整个测试流程测试发现它能自动发现以test_开头或_test结尾的文件和函数并执行它们无需复杂的配置。断言直接使用 Python 原生的assert语句进行断言失败时pytest会给出非常清晰的错误信息。夹具Fixtures这是pytest的王牌功能用于提供测试所需的固定环境或数据如初始化数据库连接、准备测试用户实现测试前置和后置操作的复用。参数化轻松实现数据驱动测试用一组数据驱动同一个测试逻辑。插件生态丰富的插件支持如pytest-html生成 HTML 报告pytest-allure生成 Allure 报告pytest-xdist实现分布式测试等。简单来说我们用requests来“做”请求和“验”响应用pytest来“组织”这些请求验证逻辑并“运行”和“报告”整个测试集。3. 第一个接口测试用例从零到一的实践3.1 选择一个合适的测试目标公开 API为了演示我们需要一个稳定、免费且无需认证的公开 API。jsonplaceholder.typicode.com是一个经典的 REST API 测试和原型设计服务我们用它来模拟一个博客帖子的接口。我们的第一个测试目标调用GET /posts/1接口获取 ID 为 1 的帖子信息并验证响应状态码为 200且返回的帖子 ID 确实是 1。3.2 编写测试文件与用例在项目根目录下创建一个名为test_get_post.py的文件。pytest默认会递归查找当前目录及子目录下所有以test_开头或_test结尾的 Python 文件。# test_get_post.py import requests def test_get_single_post_status_code(): 测试获取单个帖子接口的状态码 url https://jsonplaceholder.typicode.com/posts/1 response requests.get(url) # 使用 assert 进行断言 assert response.status_code 200, f预期状态码 200实际得到 {response.status_code} def test_get_single_post_response_data(): 测试获取单个帖子接口的响应数据 url https://jsonplaceholder.typicode.com/posts/1 response requests.get(url) # 首先确保请求成功再解析 JSON assert response.status_code 200 # 将响应内容解析为 Python 字典 post_data response.json() # 验证返回的帖子 id 是否为 1 assert post_data[id] 1, f预期帖子 id 为 1实际得到 {post_data[id]} # 可以继续验证其他字段如 userId, title 等 assert title in post_data, 响应体中应包含 title 字段 assert body in post_data, 响应体中应包含 body 字段3.3 运行测试并解读结果保存文件后在终端确保虚拟环境已激活中切换到项目根目录直接运行pytest命令pytest你会看到类似下面的输出 test session starts platform win32 -- Python 3.9.13, pytest-7.4.4, pluggy-1.3.0 rootdir: C:\path\to\api-test-demo collected 2 items test_get_post.py .. [100%] 2 passed in 1.23s collected 2 itemspytest自动发现了我们文件中的两个以test_开头的函数。..和[100%]两个点代表两个测试用例都通过了进度 100%。最后一行是总结显示总耗时和通过情况。如果断言失败了呢我们来故意写一个错误的断言试试把assert post_data[id] 1改成assert post_data[id] 2。再次运行pytest你会看到详细的失败信息包括哪一行代码出错、预期值是什么、实际值是什么这极大地便利了调试。实操心得在编写测试用例时一个函数最好只测试一个明确的逻辑点或接口。像上面的例子虽然我们可以把状态码和数据验证写在一个函数里但拆分成两个函数test_get_single_post_status_code和test_get_single_post_response_data是更好的实践。这样做的好处是当某个验证点失败时能快速定位问题且测试报告会更清晰。这就是单元测试中“单一职责”原则的体现。4. 进阶组织使用 pytest 夹具优化代码结构直接在每个测试函数里写url和requests.get会有很多重复代码。当我们需要添加请求头、处理会话或进行更复杂的设置时代码会变得臃肿。pytest的夹具Fixture系统就是用来解决这个问题的。4.1 创建基础夹具会话与基础 URL夹具是使用pytest.fixture装饰器定义的函数。它们可以被测试函数作为参数传入pytest会在执行测试前自动调用夹具函数并将其返回值注入到测试函数中。我们在项目根目录创建一个名为conftest.py的文件。这个文件名是pytest的约定用于存放被多个测试文件共享的夹具和钩子函数。# conftest.py import pytest import requests pytest.fixture def base_url(): 返回基础 URL 的夹具 return https://jsonplaceholder.typicode.com pytest.fixture def api_session(): 提供一个 requests.Session 实例的夹具。 使用 Session 可以跨请求保持某些参数如 cookies、headers提升效率。 session requests.Session() # 可以在这里为 session 设置公共的请求头例如 # session.headers.update({User-Agent: MyApiTest/1.0}) yield session # yield 之前的代码是 setup测试前置 session.close() # yield 之后的代码是 teardown测试后置清理资源4.2 重构测试用例使用夹具现在我们可以修改test_get_post.py使用这些夹具# test_get_post.py import pytest def test_get_single_post_status_code(base_url, api_session): 使用夹具优化后的状态码测试 url f{base_url}/posts/1 response api_session.get(url) assert response.status_code 200 def test_get_single_post_response_data(base_url, api_session): 使用夹具优化后的响应数据测试 url f{base_url}/posts/1 response api_session.get(url) assert response.status_code 200 post_data response.json() assert post_data[id] 1 # 更健壮的断言验证数据类型和关键字段存在 assert isinstance(post_data[title], str) and len(post_data[title]) 0 assert isinstance(post_data[body], str) and len(post_data[body]) 0这样做的好处复用与维护基础 URL 和 Session 配置在一处定义多处使用。如果未来 URL 变更只需修改conftest.py中的base_url夹具。资源管理api_session夹具使用yield确保了即使在测试失败的情况下session.close()也会被执行有利于管理网络连接资源。灵活性可以轻松地在conftest.py中为所有测试添加全局的前置操作如登录获取 token和后置清理。4.3 夹具的作用域控制生命周期夹具默认的作用域是function即每个测试函数都会调用一次。pytest支持更宽的作用域以提升性能scope”function”默认每个测试函数运行一次。scope”class”每个测试类运行一次。scope”module”每个.py文件运行一次。scope”session”整个测试会话一次pytest命令执行只运行一次。例如如果初始化一个数据库连接的成本很高且测试是只读的我们可以将其作用域设为sessionpytest.fixture(scopesession) def db_connection(): conn create_expensive_connection() yield conn conn.close()注意事项扩大夹具作用域可以提升速度但必须谨慎。如果测试会修改夹具返回的对象状态比如往一个共享的列表里添加数据那么module或session作用域的夹具可能会导致测试间相互干扰破坏测试的独立性。通常对于纯数据或无状态的服务如只读的base_url可以使用更大作用域对于有状态或需要隔离的资源使用默认的function作用域更安全。5. 实现数据驱动与参数化测试接口测试经常需要对同一个接口用多组不同的输入数据进行测试。硬编码多组测试数据会让测试函数变得冗长。pytest的pytest.mark.parametrize装饰器完美解决了这个问题。5.1 使用 parametrize 测试多个资源假设我们要测试获取不同 ID 的帖子# test_get_post.py (续) import pytest # 使用 parametrize 装饰器 pytest.mark.parametrize(post_id, expected_title_keyword, [ (1, sunt aut), # ID为1的帖子标题包含“sunt aut” (2, qui est), # ID为2的帖子标题包含“qui est” (3, ea molestias) # ID为3的帖子标题包含“ea molestias” ]) def test_get_multiple_posts(base_url, api_session, post_id, expected_title_keyword): 参数化测试验证不同ID的帖子标题包含特定关键词 url f{base_url}/posts/{post_id} response api_session.get(url) assert response.status_code 200 post_data response.json() # 验证返回的ID与请求ID一致 assert post_data[id] post_id # 验证标题中包含预期的关键词 assert expected_title_keyword in post_data[title], \ f标题中未找到关键词 {expected_title_keyword}实际标题为{post_data[title]}运行pytest -v-v参数显示详细信息你会看到这个测试函数被展开了三次执行每次使用不同的参数组合。5.2 从外部文件加载测试数据当测试数据量很大时将其放在代码中不便于管理。我们可以将数据放在单独的 JSON 或 YAML 文件中。例如创建一个test_data/posts.json[ {id: 1, expected_userId: 1, title_contains: sunt aut}, {id: 2, expected_userId: 1, title_contains: qui est}, {id: 3, expected_userId: 1, title_contains: ea molestias} ]然后在conftest.py中创建一个夹具来加载这些数据并在测试中使用# conftest.py (续) import json import os import pytest pytest.fixture(scopesession) def posts_test_data(): 从 JSON 文件加载帖子测试数据 data_file_path os.path.join(os.path.dirname(__file__), test_data, posts.json) with open(data_file_path, r, encodingutf-8) as f: data json.load(f) return data # test_get_post.py (续) import pytest def test_get_posts_with_external_data(base_url, api_session, posts_test_data): 使用外部 JSON 文件数据的测试 for test_case in posts_test_data: post_id test_case[id] url f{base_url}/posts/{post_id} response api_session.get(url) assert response.status_code 200 post_data response.json() assert post_data[id] post_id assert post_data[userId] test_case[expected_userId] assert test_case[title_contains] in post_data[title]这种方式实现了测试逻辑与测试数据的彻底分离维护数据就像维护一个配置文件一样简单。实操心得参数化是接口自动化测试的核心技术之一。但在实际项目中要避免过度参数化导致单个测试函数过于复杂和难以调试。一个好的原则是一个参数化测试只验证一个核心业务逻辑点。如果需要对同一个接口测试多种不同的场景如正常流、异常流、边界值可以考虑拆分成多个不同的测试函数或者使用pytest的pytest.mark.parametrize结合ids参数为每组数据起一个可读性强的名字这样在测试报告里更容易区分。6. 处理复杂场景POST 请求与异常测试6.1 发送 POST 请求创建资源我们之前主要测试了 GET 请求。现在来看如何测试一个创建资源的 POST 接口。jsonplaceholder也提供了/posts接口来模拟创建帖子。# test_create_post.py import pytest def test_create_post(base_url, api_session): 测试创建帖子接口 url f{base_url}/posts # 准备请求体数据 payload { title: My Test Post Title, body: This is the body content of my test post., userId: 1 } # 关键设置请求头告诉服务器我们发送的是 JSON 格式数据 headers { Content-type: application/json; charsetUTF-8, } response api_session.post(url, jsonpayload, headersheaders) # 验证状态码创建资源通常返回 201 Created assert response.status_code 201, f创建失败状态码{response.status_code} # 验证响应数据 response_data response.json() # 该模拟接口会返回一个带新ID的对象 assert id in response_data # 验证返回的数据包含了我们发送的数据 assert response_data[title] payload[title] assert response_data[body] payload[body] assert response_data[userId] payload[userId]关键点解析json参数requests.post(url, jsonpayload)是发送 JSON 数据最方便的方式。requests会自动将 Python 字典payload序列化为 JSON 字符串并设置Content-Type为application/json。上面的代码显式添加headers是为了更清晰地展示这个过程实际上只使用jsonpayload参数即可。data参数如果你需要发送表单数据application/x-www-form-urlencoded则应使用datapayload。状态码成功的 POST 请求不一定总是返回 200。遵循 RESTful 规范创建资源成功通常返回201 Created并在响应头Location字段中包含新资源的 URI。我们的测试断言应该符合接口的实际设计。6.2 模拟异常与错误测试一个健壮的测试套件不仅要测“阳光路径”还要测“雨天路径”即异常和错误情况。例如测试发送错误数据时接口是否返回预期的错误码和消息。# test_create_post.py (续) import pytest pytest.mark.parametrize(invalid_payload, expected_status_code, [ # 测试1: 缺少必填字段 title ({body: test body, userId: 1}, 400), # 假设接口会返回400 # 测试2: userId 字段类型错误 ({title: test, body: test body, userId: not_a_number}, 400), # 测试3: 发送空数据 ({}, 400), ]) def test_create_post_with_invalid_data(base_url, api_session, invalid_payload, expected_status_code): 测试使用无效数据创建帖子预期失败 url f{base_url}/posts # 注意这里我们仍然使用 json 参数即使数据是无效的 response api_session.post(url, jsoninvalid_payload) # 断言接口确实如我们预期那样处理了错误返回了对应的状态码 # 注意jsonplaceholder 是一个模拟接口它可能对所有 POST 都返回 201。 # 在实际项目中这里应该断言接口返回了预期的错误码如400, 422等。 # 下面的断言仅作为模式示例在 jsonplaceholder 上可能会失败。 # assert response.status_code expected_status_code print(f测试无效数据 {invalid_payload}实际状态码{response.status_code}) # 对于这个模拟接口我们至少可以断言它没有返回 201 assert response.status_code ! 201, f无效数据 {invalid_payload} 不应创建成功在实际项目中你需要根据接口文档来定义这些异常测试用例。测试异常流能确保你的应用程序在面对错误输入时行为可控而不是抛出服务器内部错误500。6.3 处理超时与重试网络请求可能因为各种原因失败。requests库和pytest插件可以帮助我们处理这些情况。设置请求超时这是生产级代码的必备项防止请求永远挂起。response api_session.get(url, timeout5) # 连接读取总超时5秒 # 或者分别设置连接超时和读取超时 response api_session.get(url, timeout(3.05, 27)) # (连接超时 读取超时)使用 pytest-retry 插件进行重试对于偶发性的网络抖动或服务短暂不可用可以自动重试测试。首先安装插件pip install pytest-retry。然后在测试函数或类上使用装饰器import pytest pytest.mark.flaky(reruns3, reruns_delay2) # 失败后重试3次每次间隔2秒 def test_flaky_network_api(base_url, api_session): 一个可能因网络问题偶发失败的测试自动重试 url f{base_url}/posts/1 response api_session.get(url, timeout2) assert response.status_code 200注意事项重试机制要慎用。它适用于处理真正的“偶发”问题如网络波动而不应用来掩盖测试或接口本身存在的稳定性缺陷。如果一个测试需要重试很多次才能通过那更应该去调查测试环境或被测接口本身的问题。同时对于写操作POST PUT DELETE的测试重试可能导致重复创建或修改资源需要结合接口的幂等性设计来考虑。7. 测试报告生成与常见问题排查7.1 生成 HTML 测试报告pytest本身输出简洁但生成一份美观的 HTML 报告更便于查看和分享。可以使用pytest-html插件。# 安装插件 pip install pytest-html # 运行测试并生成报告 pytest --htmlreport.html --self-contained-html--self-contained-html参数会将 CSS 样式等内联到 HTML 文件中生成一个独立的报告文件。打开report.html你可以看到清晰的测试结果汇总、通过/失败详情、甚至每个测试的执行时长。7.2 集成 Allure 生成更强大的报告Allure 是一个功能非常强大的测试报告框架支持展示测试步骤、附件如图片、日志、环境信息等。# 1. 安装 allure-pytest 插件和 Allure 命令行工具 pip install allure-pytest # 需要单独安装 Allure 命令行工具请参考 https://docs.qameta.io/allure/ # 2. 运行测试生成 Allure 结果数据 pytest --alluredir./allure-results # 3. 生成并打开 HTML 报告需要先安装 Allure 命令行 # 在项目根目录执行 allure serve ./allure-results你可以在测试中使用 Allure 装饰器来增强报告import allure import pytest allure.title(“获取特定帖子详情”) allure.feature(“帖子管理”) allure.story(“基础查询功能”) def test_get_post_with_allure(base_url, api_session): url f“{base_url}/posts/1” with allure.step(“步骤1: 发送 GET 请求”): response api_session.get(url) with allure.step(“步骤2: 验证状态码”): assert response.status_code 200 with allure.step(“步骤3: 验证响应体结构”): data response.json() assert ‘id’ in data allure.attach(response.text, name“响应体”, attachment_typeallure.attachment_type.TEXT)7.3 常见问题与排查技巧实录在实际使用pytest和requests进行接口测试时你几乎一定会遇到下面这些问题。这里记录了我的排查思路和解决方法。问题1ModuleNotFoundError: No module named ‘requests’现象运行pytest时提示找不到requests模块。原因最常见的原因是没在正确的 Python 环境下安装requests或者当前终端会话未激活项目的虚拟环境。解决检查终端提示符前是否有(venv)之类的虚拟环境标识。运行pip list查看已安装的包确认requests和pytest是否存在。如果不在请先激活虚拟环境再安装。在 PyCharm 等 IDE 中请检查项目解释器是否设置为了虚拟环境中的 Python。问题2error:too many requests, please try again later(状态码 429)现象短时间内发送大量请求到被测接口被服务器限流。原因服务器实施了速率限制Rate Limiting来防止滥用。解决降低请求频率在测试代码中引入time.sleep()在请求间加入短暂延迟。使用请求重试机制结合tenacity或urllib3的Retry机制在收到 429 状态码后等待一段时间再重试。使用 Mock 或 Stub对于开发阶段的测试可以考虑使用unittest.mock或pytest-mock来模拟接口响应避免调用真实接口。或者搭建一个测试专用的 Mock 服务器。联系服务提供方如果是测试第三方公开 API请查阅其文档关于速率限制的说明并确保遵守。如果是测试自家接口可以请开发同学在测试环境暂时放宽或关闭限流策略。问题3测试用例执行顺序导致的问题现象测试用例 A 创建了数据测试用例 B 依赖该数据但执行顺序不确定导致 B 失败。原因pytest默认的测试发现和执行顺序是随机的除非使用-p no:random这有助于发现测试间隐藏的依赖但有时我们需要控制顺序。解决避免测试间依赖这是最佳实践。每个测试都应该是独立的自己准备所需的数据并在测试结束后清理。可以使用夹具的setup和teardown功能即yield前后的代码来保证。使用pytest-ordering插件如果确实需要控制顺序如集成测试流程可以安装pip install pytest-ordering然后使用装饰器pytest.mark.run(order1)。设计可独立运行的测试集将有关联的测试组织在一个类中并利用类的setup_class和teardown_class方法。问题4如何测试需要认证如 Token的接口解决利用conftest.py中的夹具来处理认证逻辑。# conftest.py import pytest pytest.fixture(scopesession) def auth_token(api_session, base_url): 获取认证令牌的夹具作用域为session避免重复登录 login_url f{base_url}/auth/login login_data {username: testuser, password: testpass} resp api_session.post(login_url, jsonlogin_data) assert resp.status_code 200 token resp.json()[access_token] # 将token设置到session的headers中后续所有请求都会自动携带 api_session.headers.update({Authorization: fBearer {token}}) return token在测试函数中只需要引入auth_token夹具即使函数体没用到它pytest就会在执行测试前先运行这个夹具从而完成认证。因为api_session被更新了请求头所以后续所有使用这个api_session的请求都会自动带上 Token。问题5Pycharm 中运行 pytest用例标题和参数化参数显示换行或混乱现象在 PyCharm 的测试运行工具窗口里参数化测试的用例名很长导致显示不美观。原因PyCharm 对长测试名的渲染问题。解决在pytest.mark.parametrize中使用ids参数为每组参数提供一个简短的、可读的标识符。pytest.mark.parametrize(post_id, keyword, [(1, aut), (2, est)], ids[post_1, post_2]) def test_example(post_id, keyword): ...或者在 PyCharm 的设置中调整测试运行器窗口的显示列宽。这更多是一个显示问题不影响测试的实际执行。从最简单的 GET 请求验证到使用夹具优化结构再到数据驱动、异常测试和报告生成我们一步步搭建了一个虽小但五脏俱全的接口自动化测试框架雏形。这套组合的核心优势在于其极致的灵活性和 Python 生态的强大支撑。当你熟悉了这些基础模式后可以很容易地扩展出更多功能比如用pytest-xdist并行跑测试提升效率用pytest-base-url插件管理不同环境的基础地址或者将测试数据完全外部化到 Excel 或数据库中。我个人最深的体会是接口自动化测试的难点往往不在于工具本身而在于测试用例的设计、测试数据的维护以及测试环境的稳定性。pytest和requests给了你一套强大的“乐高积木”但如何搭建出稳固、可维护、高效的测试大厦还需要你在实践中不断思考和迭代。开始时不必追求大而全从一个核心接口、几个关键场景入手让自动化测试先跑起来产生价值再逐步完善这才是可持续的落地方式。