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

高级测试工程师课程(第14章):接口自动化测试实战——requests + pytest + 数据驱动 + mock,13条用例全过 高级测试工程师课程第14章接口自动化测试实战——requests pytest 数据驱动 mock13条用例全过前言接口自动化是测试工程师的核心竞争力相比 UI 自动化它更稳定、更快、ROI 更高。本章我在 Ubuntu 24.04 服务器上以公开 APIhttpbin.org为被测系统完整实操了课程第 14 章内容RESTful 概念、requests 库七大核心用法、pytest 组织接口用例fixture 管理 base_url 与 token、数据驱动、状态码响应体响应时间三维断言、responses 库 mock 测试最后用 pytest-html 生成报告。13 条用例全部真实执行通过全部输出为服务器真实回显。一、实验环境项目配置云主机华为云 ECS8 vCPU / 14GB 内存操作系统Ubuntu 24.04.4 LTS (noble)Python3.12.3虚拟环境 /root/venvpytest9.1.1 / pytest-html 4.2.0requests2.34.2responses0.26.3mock 库被测系统https://httpbin.org公开 HTTP 调试服务实操目录/root/api-lab1.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 time1.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 time0.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/jsonRESTful 的核心思想是用 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.py2.1 GET 带参数 POST JSONimportrequests BASEhttps://httpbin.orgrrequests.get(f{BASE}/get,params{name:张三,page:1},timeout10)rrequests.post(f{BASE}/post,json{username:tester,password:123456},timeout10)真实输出 1. GET 带参数 URL: https://httpbin.org/get?name%E5%BC%A0%E4%B8%89page1 状态码: 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 Authtokenmy-test-token-2026rrequests.get(f{BASE}/bearer,headers{Authorization:fBearer{token}},timeout10)rrequests.get(f{BASE}/basic-auth/demo/123456,auth(demo,123456),timeout10)真实输出 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 保持 Cookiesrequests.Session()s.get(f{BASE}/cookies/set/session_id/abc123,timeout10)rs.get(f{BASE}/cookies,timeout10)真实输出 5. Session 保持 Cookie session读回cookies: {cookies: {session_id: abc123}} 裸requests读cookies: {cookies: {}} - 没有会话所以为空解读同一个 Session 内 cookie 被自动保存并携带读回了 session_idabc123而裸 requests 没有会话读到的是空——对照实验直观证明了 Session 的 cookie 持久化能力 ✅。Session 还带连接池复用接口自动化框架中全局只应创建一个 Session。2.4 超时与重试# 超时try:requests.get(f{BASE}/delay/5,timeout1)exceptrequests.exceptions.Timeoutase:print(f如期捕获超时:{type(e).__name__})# 重试fromrequests.adaptersimportHTTPAdapterfromurllib3.util.retryimportRetry retryRetry(total3,backoff_factor0.3,status_forcelist[500,502,503,504],raise_on_statusFalse)sessionrequests.Session()session.mount(https://,HTTPAdapter(max_retriesretry))rsession.get(f{BASE}/status/500,timeout10)真实输出 6. 超时控制 如期捕获超时: ReadTimeout: HTTPSConnectionPool(hosthttpbin.org, port443): Read timed out. (read timeout 7. 重试机制 HTTPAdapter Retry 请求/status/500 重试3次后最终状态码: 500 正常接口不受影响: 200解读/delay/5接口延迟 5 秒timeout1如期抛出ReadTimeout✅——所有接口请求必须设超时否则一个假死的服务会拖垮整个测试套件。重试机制对 500 自动重试 3 次间隔按 backoff_factor 递增最终仍失败则返回最后的 500 响应正常接口不受影响。三、用 pytest 组织接口用例3.1 conftest.pyfixture 管理 base_url 和 tokenimportpytestimportrequestspytest.fixture(scopesession)defbase_url():returnhttps://httpbin.orgpytest.fixture(scopesession)defsession():整个会话复用同一个 requests.Session连接池srequests.Session()s.headers.update({User-Agent:api-auto-test/1.0})yields s.close()pytest.fixture(scopesession)deftoken(base_url,session):登录fixture验证身份 - 返回token供后续用例使用rsession.get(f{base_url}/basic-auth/demo/123456,auth(demo,123456),timeout10)assertr.status_code200,f登录失败:{r.status_code}tktoken-from-loginprint(f\n[fixture] 登录成功, 签发token:{tk})returntk设计要点三个 fixture 都是session scope——base_url 全局一份、Session 全局一个连接池复用、token 只在会话开始登录一次后续用例直接注入使用。fixture 之间可以互相依赖token 依赖 base_url 和 sessionpytest 自动解析依赖顺序。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_ELAPSED5.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_ELAPSEDpytest.mark.parametrize(path,params,expect_code,GET_CASES,ids[fGET{p}forp,_,_inGET_CASES])deftest_get_cases(base_url,session,path,params,expect_code):rsession.get(f{base_url}{path},paramsparams,timeout10)# 断言1: 状态码assertr.status_codeexpect_code,f期望{expect_code}, 实际{r.status_code}# 断言2: 响应时间assertr.elapsed.total_seconds()MAX_ELAPSED,响应超时# 断言3: 响应体字段ifexpect_code200:bodyr.json()assertbody[args](paramsor{})asserthttpbin.orginbody[url]deftest_bearer_token(base_url,session,token):依赖token fixture的鉴权接口rsession.get(f{base_url}/bearer,headers{Authorization:fBearer{token}},timeout10)assertr.status_code200assertr.json()[authenticated]isTruedeftest_no_token_rejected(base_url,session):反向用例不带token应返回401rsession.get(f{base_url}/bearer,timeout10)assertr.status_code401接口断言黄金三件套状态码 响应体关键字段 响应时间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库在传输层拦截请求importresponsesimportrequestsresponses.activatedeftest_mock_login_and_retry():前两次返回500, 第三次成功 - 验证客户端重试逻辑urlhttps://fake-api.local/loginresponses.add(responses.POST,url,json{error:server error},status500)responses.add(responses.POST,url,json{error:server error},status500)responses.add(responses.POST,url,json{token:t-123},status200)for_inrange(3):rrequests.post(url,json{u:demo},timeout5)ifr.status_code200:breakassertr.status_code200assertr.json()[token]t-123assertlen(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 --htmlapi_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--htmlapi_report.html --self-contained-html--junitxmljunit.xml# 退出码即测试结果0全过非0有失败CI自动判红两个产物分工junit.xml给 CI 平台解析展示趋势图通过率、耗时走势api_report.html作为构建制品归档供人查看。pytest 的退出码设计0 全过 / 1 有失败 / 2 执行中断 / 5 没收集到用例让流水线不需要任何解析逻辑就能判定构建状态这也是为什么测试框架选型时退出码语义是个容易被忽略但很关键的评价维度。七、踩坑记录坑1Retry 重试耗尽后抛 RetryError本次实操真实遇到现象第一次写重试演示时没加raise_on_statusFalse请求/status/500直接炸了requests.exceptions.RetryError: HTTPSConnectionPool(hosthttpbin.org, port443): Max retries exceeded with url: /status/500 (Caused by ResponseError(too many 500 error responses))原因urllib3 的Retry默认raise_on_statusTrue重试次数耗尽后抛异常而不是返回最后的响应。解决Retry(total3, status_forcelist[500,502,503,504], raise_on_statusFalse)修复后正常输出重试3次后最终状态码: 500 ✅。如果希望重试后仍失败就抛异常比如登录接口反而应该保持默认值——按业务语义选择。坑2responses 库没有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.14sjsonplaceholder 200/0.90s选 httpbin ✅requests 七大用法GET参数/POST JSON/Bearer/BasicAuth/Session Cookie/超时/重试 全部实证 ✅fixture 管理session scope 的 base_url/session/token 三级依赖注入 ✅数据驱动Python 数据文件 parametrize4 GET 3 POST 自动展开 ✅三维断言状态码响应体字段r.elapsed 响应时间 ✅反向用例无 token 必须 401 ✅mockresponses 拦截不存在域名重试场景验证 calls3 ✅测试报告api_report.html 39KB13 passed ✅至此一个最小但完整的接口自动化框架雏形已经搭好数据与代码分离、fixture 管理上下文、正反用例覆盖、mock 兜底异常场景、报告可归档。在此基础上按需接入 YAML 数据驱动、Allure 报告、Jenkins 流水线即可演进为生产级框架。参考链接requests 官方文档https://requests.readthedocs.io/httpbin 在线服务https://httpbin.orgresponses 库https://pypi.org/project/responses/pytest 官方文档https://docs.pytest.org/