高级测试工程师课程(第14章):接口自动化测试实战——requests + pytest + 数据驱动 + mock,13条用例全过
2026/9/8 4:03:42 网站建设 项目流程

高级测试工程师课程(第14章):接口自动化测试实战——requests + pytest + 数据驱动 + mock,13条用例全过

前言

接口自动化是测试工程师的核心竞争力:相比 UI 自动化,它更稳定、更快、ROI 更高。本章我在 Ubuntu 24.04 服务器上,以公开 APIhttpbin.org为被测系统,完整实操了课程第 14 章内容:RESTful 概念、requests 库七大核心用法、pytest 组织接口用例(fixture 管理 base_url 与 token、数据驱动、状态码+响应体+响应时间三维断言)、responses 库 mock 测试,最后用 pytest-html 生成报告。13 条用例全部真实执行通过,全部输出为服务器真实回显。

一、实验环境

项目配置
云主机华为云 ECS,8 vCPU / 14GB 内存
操作系统Ubuntu 24.04.4 LTS (noble)
Python3.12.3(虚拟环境 /root/venv)
pytest9.1.1 / pytest-html 4.2.0
requests2.34.2
responses0.26.3(mock 库)
被测系统https://httpbin.org(公开 HTTP 调试服务)
实操目录/root/api-lab

1.1 被测系统连通性确认(先行验证)

动手前先 curl 探测候选公开 API,确保网络可达:

$ curl -sS -o /dev/null -w 'httpbin: %{http_code} time=%{time_total}s\n' --max-time 10 https://httpbin.org/get httpbin: 200 time=1.139899s $ curl -sS -o /dev/null -w 'jsonplaceholder: %{http_code} time=%{time_total}s\n' --max-time 10 https://jsonplaceholder.typicode.com/users/1 jsonplaceholder: 200 time=0.898621s

两个都通 ✅,按优先级选用httpbin.org(它能回显请求细节,最适合教学演示)。如果连公开 API 都不通,兜底方案是用 Flask 在本机自建一个带 GET/POST/登录发 token 的被测服务,完全可控。

1.2 RESTful 概念速览

要素说明本文示例
资源(Resource)一切皆资源,用 URI 标识/users/1、/post
统一接口GET查询/POST创建/PUT更新/DELETE删除GET /get、POST /post
无状态服务端不保存会话,状态靠客户端携带Authorization: Bearer token
表现形式JSON/XML 等,Content-Type 协商application/json

RESTful 的核心思想是"用 HTTP 协议的语义表达操作":GET 不该有副作用,POST 创建资源返回 201,资源不存在返回 404,未授权返回 401,权限不足返回 403。接口测试本质上就是在验证服务端是否遵守了这套契约——所以我们的用例里既有/status/404/status/500这样的状态码专项,也有"不带 token 必须 401"的反向鉴权用例。判断一个接口设计得是否 RESTful,最简单的办法是看 URI 里有没有动词:/getUserInfo是 RPC 风格,GET /users/1才是 REST 风格。

1.3 项目目录结构

本次实操的/root/api-lab目录组织如下,这是一个可直接复用的最小框架骨架:

/root/api-lab ├── conftest.py # fixture层:base_url / session / token ├── api_data.py # 数据层:用例数据与代码分离 ├── test_api.py # 用例层:真实接口测试(10条) ├── test_mock_responses.py # mock层:不依赖真实服务的测试(3条) ├── requests_demo.py # requests基础用法演示脚本 └── api_report.html # pytest-html生成的测试报告

分层原则:fixture 管上下文,数据文件管用例,测试函数只管发请求和断言。任何一层要改动都不影响其他层——换环境改 conftest,加用例改数据文件,改断言策略才动测试函数。

二、requests 库七大核心用法实操(requests_demo.py)

2.1 GET 带参数 + POST JSON

importrequests BASE="https://httpbin.org"r=requests.get(f"{BASE}/get",params={"name":"张三","page":1},timeout=10)r=requests.post(f"{BASE}/post",json={"username":"tester","password":"123456"},timeout=10)

真实输出:

========== 1. GET 带参数 ========== URL: https://httpbin.org/get?name=%E5%BC%A0%E4%B8%89&page=1 状态码: 200 服务端收到的args: {'name': '张三', 'page': '1'} 响应时间: 2.367s ========== 2. POST JSON ========== 状态码: 200 服务端收到的json: {'password': '123456', 'username': 'tester'} Content-Type: application/json

解读:params自动 URL 编码(张三 → %E5%BC%A0%E4%B8%89)✅;json=参数自动序列化并带上Content-Type: application/json✅。httpbin 把服务端收到的数据原样回显,方便我们验证"客户端发出去的"和"服务端收到的"是否一致。

2.2 鉴权:Bearer Token 与 Basic Auth

token="my-test-token-2026"r=requests.get(f"{BASE}/bearer",headers={"Authorization":f"Bearer{token}"},timeout=10)r=requests.get(f"{BASE}/basic-auth/demo/123456",auth=("demo","123456"),timeout=10)

真实输出:

========== 3. 鉴权:Bearer Token ========== 带token: 200 -> {'authenticated': True, 'token': 'my-test-token-2026'} 不带token: 401 -> ========== 4. Basic Auth(模拟登录换token) ========== 正确口令: 200 {'authenticated': True, 'user': 'demo'} 错误口令: 401

解读:带 token 200、不带 401;正确口令 200、错误口令 401。这四组正反结果完整覆盖了鉴权接口的测试矩阵 ✅。auth=(user, pwd)是 requests 的 Basic Auth 快捷写法。

2.3 Session 保持 Cookie

s=requests.Session()s.get(f"{BASE}/cookies/set/session_id/abc123",timeout=10)r=s.get(f"{BASE}/cookies",timeout=10)

真实输出:

========== 5. Session 保持 Cookie ========== session读回cookies: {'cookies': {'session_id': 'abc123'}} 裸requests读cookies: {'cookies': {}} <- 没有会话所以为空

解读:同一个 Session 内 cookie 被自动保存并携带(读回了 session_id=abc123),而裸 requests 没有会话读到的是空——对照实验直观证明了 Session 的 cookie 持久化能力 ✅。Session 还带连接池复用,接口自动化框架中全局只应创建一个 Session。

2.4 超时与重试

# 超时try:requests.get(f"{BASE}/delay/5",timeout=1)exceptrequests.exceptions.Timeoutase:print(f"如期捕获超时:{type(e).__name__}")# 重试fromrequests.adaptersimportHTTPAdapterfromurllib3.util.retryimportRetry retry=Retry(total=3,backoff_factor=0.3,status_forcelist=[500,502,503,504],raise_on_status=False)session=requests.Session()session.mount("https://",HTTPAdapter(max_retries=retry))r=session.get(f"{BASE}/status/500",timeout=10)

真实输出:

========== 6. 超时控制 ========== 如期捕获超时: ReadTimeout: HTTPSConnectionPool(host='httpbin.org', port=443): Read timed out. (read timeout ========== 7. 重试机制 HTTPAdapter + Retry ========== 请求/status/500 重试3次后最终状态码: 500 正常接口不受影响: 200

解读:/delay/5接口延迟 5 秒,timeout=1如期抛出ReadTimeout✅——所有接口请求必须设超时,否则一个假死的服务会拖垮整个测试套件。重试机制对 500 自动重试 3 次(间隔按 backoff_factor 递增),最终仍失败则返回最后的 500 响应;正常接口不受影响。

三、用 pytest 组织接口用例

3.1 conftest.py:fixture 管理 base_url 和 token

importpytestimportrequests@pytest.fixture(scope="session")defbase_url():return"https://httpbin.org"@pytest.fixture(scope="session")defsession():"""整个会话复用同一个 requests.Session(连接池)"""s=requests.Session()s.headers.update({"User-Agent":"api-auto-test/1.0"})yields s.close()@pytest.fixture(scope="session")deftoken(base_url,session):"""登录fixture:验证身份 -> 返回token,供后续用例使用"""r=session.get(f"{base_url}/basic-auth/demo/123456",auth=("demo","123456"),timeout=10)assertr.status_code==200,f"登录失败:{r.status_code}"tk="token-from-login"print(f"\n[fixture] 登录成功, 签发token:{tk}")returntk

设计要点:三个 fixture 都是session scope——base_url 全局一份、Session 全局一个(连接池复用)、token 只在会话开始登录一次,后续用例直接注入使用。fixture 之间可以互相依赖(token 依赖 base_url 和 session),pytest 自动解析依赖顺序。

3.2 数据驱动:api_data.py

# GET 用例: (路径, 参数, 预期状态码)GET_CASES=[("/get",{"name":"zhangsan","age":"28"},200),("/get",{"keyword":"pytest"},200),("/status/404",None,404),("/status/500",None,500),]# POST 用例: (路径, json体, 预期状态码, 预期回显字段)POST_CASES=[("/post",{"username":"u1","pwd":"p1"},200,"u1"),("/post",{"username":"u2","pwd":"p2"},200,"u2"),("/post",{"keyword":"接口自动化"},200,"接口自动化"),]MAX_ELAPSED=5.0# 响应时间阈值(秒)

数据与代码分离:加用例只改数据文件,不动测试逻辑。数据量大了可以平移成 YAML/Excel/数据库,加载方式不同而已。

为什么推荐从 Python 数据文件起步而不是直接上 YAML?三个原因:一是零依赖,不需要装 PyYAML 也不用手写解析;二是 Python 数据结构支持注释和计算(比如用datetime.now()动态生成时间戳字段),静态 YAML 做不到;三是 IDE 有语法检查,写错一个括号立刻飘红,而 YAML 的缩进错误往往运行时才暴露。当然,当用例规模上到几百条、需要非开发人员维护数据时,YAML/Excel 的可读性优势就体现出来了——届时只需在 conftest 里加一个读文件的 fixture,把读出来的 list 传给 parametrize,测试函数一行都不用改。这种"数据加载方式可插拔"的设计正是数据驱动框架的精髓。

3.3 测试用例:三维断言

importpytestfromapi_dataimportGET_CASES,POST_CASES,MAX_ELAPSED@pytest.mark.parametrize("path,params,expect_code",GET_CASES,ids=[f"GET{p}"forp,_,_inGET_CASES])deftest_get_cases(base_url,session,path,params,expect_code):r=session.get(f"{base_url}{path}",params=params,timeout=10)# 断言1: 状态码assertr.status_code==expect_code,f"期望{expect_code}, 实际{r.status_code}"# 断言2: 响应时间assertr.elapsed.total_seconds()<MAX_ELAPSED,"响应超时"# 断言3: 响应体字段ifexpect_code==200:body=r.json()assertbody["args"]==(paramsor{})assert"httpbin.org"inbody["url"]deftest_bearer_token(base_url,session,token):"""依赖token fixture的鉴权接口"""r=session.get(f"{base_url}/bearer",headers={"Authorization":f"Bearer{token}"},timeout=10)assertr.status_code==200assertr.json()["authenticated"]isTruedeftest_no_token_rejected(base_url,session):"""反向用例:不带token应返回401"""r=session.get(f"{base_url}/bearer",timeout=10)assertr.status_code==401

接口断言黄金三件套:状态码 + 响应体关键字段 + 响应时间r.elapsed是 requests 自带的服务端响应耗时,比手动计时准确)。同时注意要覆盖反向用例(不带 token 必须 401),只测正向等于没测鉴权。

3.4 断言策略的取舍

实际项目中"响应体断言到什么程度"是个值得讨论的问题,三种粒度的取舍:

粒度做法优点缺点
全量比对整个响应体与预期 JSON 全等最严格极脆弱,时间戳/id 等动态字段必挂 ❌
关键字段断言只断言业务关心的字段稳定且表达业务意图 ✅需要人工识别关键字段
Schema 校验用 jsonschema 校验结构类型能发现字段缺失/类型错误不校验具体值

本章采用的是"关键字段断言":POST 用例断言body["json"] == payload(回显必须等于请求体),GET 用例断言body["args"]与参数一致。对于 httpbin 这种回显型接口,回显相等本身就是最强断言。另外断言信息要写清楚:assert r.status_code == expect_code, f"期望{expect_code}, 实际{r.status_code}"——失败时直接看到期望和实际,不用回去翻代码,这个小习惯能让排障效率提升一个量级。

四、真实执行结果(pytest -v 全文)

$ cd /root/api-lab && /root/venv/bin/pytest -v ============================= test session starts ============================== platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0 -- /root/venv/bin/python3 cachedir: .pytest_cache metadata: {'Python': '3.12.3', 'Platform': 'Linux-6.8.0-106-generic-x86_64-with-glibc2.39', 'Packages': {'pytest': '9.1.1', 'pluggy': '1.6.0'}, 'Plugins': {'html': '4.2.0', 'metadata': '3.1.1'}} rootdir: /root/api-lab plugins: html-4.2.0, metadata-3.1.1 collecting ... collected 13 items test_api.py::test_get_cases[GET /get0] PASSED [ 7%] test_api.py::test_get_cases[GET /get1] PASSED [ 15%] test_api.py::test_get_cases[GET /status/404] PASSED [ 23%] test_api.py::test_get_cases[GET /status/500] PASSED [ 30%] test_api.py::test_post_cases[POST u1] PASSED [ 38%] test_api.py::test_post_cases[POST u2] PASSED [ 46%] test_api.py::test_post_cases[POST kw] PASSED [ 53%] test_api.py::test_bearer_token PASSED [ 61%] test_api.py::test_no_token_rejected PASSED [ 69%] test_api.py::test_response_headers PASSED [ 76%] test_mock_responses.py::test_mock_get_user PASSED [ 84%] test_mock_responses.py::test_mock_login_and_retry PASSED [ 92%] test_mock_responses.py::test_mock_404 PASSED [100%] ============================== 13 passed in 4.71s ==============================

13 条用例全部通过✅:4 条 GET(含 404/500 异常状态码)、3 条 POST(含中文 JSON)、2 条鉴权正反、1 条响应头、3 条 mock。跨国访问 httpbin 全量仅 4.71s。

五、mock 测试:responses 库(test_mock_responses.py)

真实服务不可用、异常场景难构造时,用responses库在传输层拦截请求:

importresponsesimportrequests@responses.activatedeftest_mock_login_and_retry():"""前两次返回500, 第三次成功 -> 验证客户端重试逻辑"""url="https://fake-api.local/login"responses.add(responses.POST,url,json={"error":"server error"},status=500)responses.add(responses.POST,url,json={"error":"server error"},status=500)responses.add(responses.POST,url,json={"token":"t-123"},status=200)for_inrange(3):r=requests.post(url,json={"u":"demo"},timeout=5)ifr.status_code==200:breakassertr.status_code==200assertr.json()["token"]=="t-123"assertlen(responses.calls)==3# 验证确实请求了3次

运行结果(已在上面的全量输出中):

test_mock_responses.py::test_mock_get_user PASSED [ 84%] test_mock_responses.py::test_mock_login_and_retry PASSED [ 92%] test_mock_responses.py::test_mock_404 PASSED [100%]

解读:

  • https://fake-api.local根本不存在的域名,但测试照常通过——请求在发出前被 responses 拦截 ✅。
  • 同一个 URL 注册多个响应时按注册顺序依次消费,完美模拟"服务抖动后恢复"的重试验证场景。
  • responses.calls记录所有请求,可断言请求次数、请求头、请求体,实现"行为验证"。

mock 在接口自动化中的定位需要说清楚:mock 测试不能替代真实接口测试,两者解决的是不同问题。真实接口测试验证的是"客户端与服务端的契约是否成立",mock 测试验证的是"客户端代码在各种响应下的行为是否正确"——比如重试逻辑、超时处理、异常分支。真实服务里要构造"连续两次 500 后恢复"几乎不可能,但业务代码里恰恰有这种分支要覆盖,这就是 mock 的不可替代价值。成熟的测试金字塔里,两者应该是互补关系:主链路用真实接口(或测试环境),异常分支和第三方依赖用 mock。

六、生成测试报告

$ cd /root/api-lab && /root/venv/bin/pytest --html=api_report.html --self-contained-html --------- Generated html report: file:///root/api-lab/api_report.html ---------- ============================== 13 passed in 6.50s ============================== $ ls -l api_report.html -rw-r--r-- 1 root root 39751 Sep 5 17:17 api_report.html

单文件 HTML 报告(39KB),包含环境信息、13 条用例的通过状态与耗时,可直接归档或接入 CI 制品。

6.1 接入 CI 的最小配置

要把这套用例接进 Jenkins/GitLab CI,只需要三步:

pipinstall-rrequirements.txt# pytest requests responses pytest-htmlpytest--html=api_report.html --self-contained-html--junitxml=junit.xml# 退出码即测试结果:0=全过,非0=有失败,CI自动判红

两个产物分工:junit.xml给 CI 平台解析展示趋势图(通过率、耗时走势),api_report.html作为构建制品归档供人查看。pytest 的退出码设计(0 全过 / 1 有失败 / 2 执行中断 / 5 没收集到用例)让流水线不需要任何解析逻辑就能判定构建状态,这也是为什么测试框架选型时"退出码语义"是个容易被忽略但很关键的评价维度。

七、踩坑记录

坑1:Retry 重试耗尽后抛 RetryError(本次实操真实遇到)

现象:第一次写重试演示时没加raise_on_status=False,请求/status/500直接炸了:

requests.exceptions.RetryError: HTTPSConnectionPool(host='httpbin.org', port=443): Max retries exceeded with url: /status/500 (Caused by ResponseError('too many 500 error responses'))

原因:urllib3 的Retry默认raise_on_status=True,重试次数耗尽后抛异常而不是返回最后的响应

解决Retry(total=3, status_forcelist=[500,502,503,504], raise_on_status=False),修复后正常输出"重试3次后最终状态码: 500" ✅。如果希望"重试后仍失败就抛异常"(比如登录接口),反而应该保持默认值——按业务语义选择。

坑2:responses 库没有version属性

import responses; responses.__version__会抛AttributeError❌。查看版本用pip show responses(本次实测 0.26.3)。小众库不要假设有__version__

坑3:响应时间断言阈值要留余量

跨国访问 httpbin 平均 1~2s(实测 GET 首次 2.367s),阈值若设 1s 会大面积误报失败 ❌。本次设 5s 并通过 Session 连接池复用降低后续请求耗时 ✅。生产项目建议按 P95 基线设定阈值。

八、总结

知识点实操结果
连通性探测httpbin 200/1.14s,jsonplaceholder 200/0.90s,选 httpbin ✅
requests 七大用法GET参数/POST JSON/Bearer/BasicAuth/Session Cookie/超时/重试 全部实证 ✅
fixture 管理session scope 的 base_url/session/token 三级依赖注入 ✅
数据驱动Python 数据文件 + parametrize,4 GET + 3 POST 自动展开 ✅
三维断言状态码+响应体字段+r.elapsed 响应时间 ✅
反向用例无 token 必须 401 ✅
mockresponses 拦截不存在域名,重试场景验证 calls==3 ✅
测试报告api_report.html 39KB,13 passed ✅

至此,一个最小但完整的接口自动化框架雏形已经搭好:数据与代码分离、fixture 管理上下文、正反用例覆盖、mock 兜底异常场景、报告可归档。在此基础上按需接入 YAML 数据驱动、Allure 报告、Jenkins 流水线即可演进为生产级框架。

参考链接

  • requests 官方文档:https://requests.readthedocs.io/
  • httpbin 在线服务:https://httpbin.org
  • responses 库:https://pypi.org/project/responses/
  • pytest 官方文档:https://docs.pytest.org/

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

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

立即咨询