高级测试工程师课程(第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) |
| Python | 3.12.3(虚拟环境 /root/venv) |
| pytest | 9.1.1 / pytest-html 4.2.0 |
| requests | 2.34.2 |
| responses | 0.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 ✅ |
| mock | responses 拦截不存在域名,重试场景验证 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/