
做接口自动化测试这两年我最大的感触是95%的人都会用 Requests但真正能把这个库用好的人可能不到 5%。很多人用 Requests 的方式永远停留在两行代码上resp requests.post(https://xxx.com/api/login, json{username: admin, password: 123456}) print(resp.json())能跑通但也就到此为止了。一旦遇到登录态丢失、连接池报错、429 限流、SSL 证书校验失败、超时重试不会配、Session 和普通请求到底该用哪个……这些问题出现时大多数人的做法是去搜索引擎复制一段代码能解决就继续往下写不能解决就换一个方案。至于底层到底发生了什么始终是一团迷雾。这篇文章我想换一种讲法直接从 Requests 源码入手把一条 HTTP 请求从发出到返回的完整链路拆开看然后再落到接口自动化测试的真实场景从登录、鉴权、业务接口到断言封装一步步搭出一个可复用的工程。先给一个明确判断对于 Python 接口自动化测试来说Requests 是所有工具链里最值得读源码的基础库。它不庞大核心模块就那么几个但它是整个测试框架的“地基”。把地基打牢不管是写 pytest 测试用例、封装 APIClient还是排查线上诡异问题你都会比别人多一层认知优势。1. 为什么接口自动化测试必须理解 Requests 源码先不急着看代码我们想清楚一个问题接口自动化测试和普通脚本调用接口本质区别是什么普通脚本调用接口核心目标是“把请求发出去”比如爬虫抓一个页面或调试一个临时接口。它不关心状态管理不关心请求链路不关心可重复执行。而接口自动化测试不一样。它要求测试用例可以重复执行且每次执行结果稳定登录态可以被多个用例共享而不是每个用例单独登录请求失败时能自动重试而不是直接报错响应结果能被结构化成断言对象测试脚本要进入 CI/CD 流程具备可维护性。这就意味着你不能只“会用” Requests你必须理解它内部的机制才能在它出现问题时快速定位。举几个真实场景场景一登录态丢失你写了一个 login 接口获取 token然后下一个测试用例带着 token 去请求用户信息接口结果发现 token 是拿到的但请求发出去就是 401。排查半天发现你每次都在用requests.get()而不是session.get()token 虽然拼到了 header 里但 Session 的 cookie 机制完全没被触发。场景二并发一高就报错性能测试跑 50 个并发Requests 频繁报Connection pool is full或者Max retries exceeded。你以为是服务器扛不住实际是客户端连接池默认只有 10 个连接你的并发量超过了连接池上限。场景三429 限流接口返回 429代码直接抛异常任务中断。你需要在代码里配置重试和退避策略但不知道重试逻辑是 Requests 做的还是 urllib3 做的改了半天没效果。这些问题的答案全在 Requests 源码里。所谓“手撕源码”不是让你去背每一行代码而是让你看懂调用链。当你脑海中有一条清晰的链路图——从requests.get()到最终 HTTP 请求发出中间经历了哪些对象、哪些方法、哪些可配置点——你在使用 Requests 时就不再是“盲人摸象”。2. Requests 核心架构一条请求穿越的四个层级Requests 库虽然使用起来非常简单但它的内部是分层的。一条 HTTP 请求从发出到返回核心路径会依次穿越四个层级层级核心模块职责API 层requests/api.py提供get()、post()等顶层函数面向使用者Session 层requests/sessions.py管理 Cookie、Header、Hook、证书等会话状态Adapter 层requests/adapters.py负责具体传输连接池管理、重试逻辑底层库urllib3真正的 HTTP 连接建立、请求发送、响应读取很多人不知道** Requests 并不是自己在底层发 HTTP 请求的它真正依赖的是 urllib3。** Requests 做的事情是把 URL、参数、Header、Cookie、超时、证书、代理这些复杂配置统一封装成一套友好 API然后交给 urllib3 去执行。换句话说Requests 是“大脑”负责组装请求、管理状态、处理策略。urllib3 是“手脚”负责真正把数据包发出去并接收响应。理解这一点非常重要。因为很多参数——比如重试、连接池大小——其实是透传给 urllib3 的。你只在 Requests 层找配置永远找不到。让我们从最常用的一行代码开始看看它到底经历了什么requests.get(https://api.example.com/users)这行代码的调用链是这样的requests.api.get() - request(get, url) - Session().request(get, url) - Request() 对象构建 - Session.prepare_request() - PreparedRequest 对象 - Session.send() - HTTPAdapter.send() - urllib3 PoolManager 连接池 - HTTPConnectionPool.urlopen() - HTTP 请求真正发出可以看到你表面上只调用了一个函数实际上内部经历了近 10 个步骤。接口自动化测试真正要深入掌握的就是这个链条中 Session 层和 Adapter 层的机制。3. 从源码理解 Session 与连接复用机制在接口自动化测试中最常见的错误之一是每个测试用例都直接调用requests.get()或requests.post()导致登录态丢失、连接无法复用。先看requests/api.py的源码实现。虽然具体版本的源码行数有差异但核心逻辑是非常稳定的# requests/api.py逻辑简化版便于理解调用链 def request(method, url, **kwargs): with sessions.Session() as session: return session.request(methodmethod, urlurl, **kwargs) def get(url, paramsNone, **kwargs): return request(get, url, paramsparams, **kwargs) def post(url, dataNone, jsonNone, **kwargs): return request(post, url, datadata, jsonjson, **kwargs)看到问题了吗每次调用requests.get()或requests.post()内部都会临时创建了一个新的 Session并在请求完成后立即关闭。这意味着Cookie 无法自动保存。上一次响应中Set-Cookie设置的内容不会带到下一次请求中连接无法复用。每个请求都要重新建立 TCP 连接甚至重新做 TLS 握手性能非常差Header 无法统一管理。你必须在每个请求里重复传同一个 Header。这就是为什么接口自动化测试中几乎都会使用requests.Session()来发起请求。我们再看sessions.py中Session.request()的核心逻辑# requests/sessions.py逻辑简化版 class Session(SessionRedirectMixin): def __init__(self): self.headers default_headers() self.cookies cookiejar_from_dict({}) self.auth None self.proxies {} self.verify True self.cert None self.max_redirects DEFAULT_REDIRECT_LIMIT self.adapters OrderedDict() # 默认为 http 和 https 注册 HTTPAdapter self.mount(https://, HTTPAdapter()) self.mount(http://, HTTPAdapter()) def request(self, method, url, paramsNone, dataNone, headersNone, cookiesNone, filesNone, authNone, timeoutNone, allow_redirectsTrue, proxiesNone, hooksNone, streamNone, verifyNone, certNone, jsonNone): # 1. 构造 Request 对象 req Request( methodmethod.upper(), urlurl, headersheaders, filesfiles, datadata or {}, jsonjson, paramsparams or {}, authauth, cookiescookies, hookshooks, ) # 2. 生成 PreparedRequest prep self.prepare_request(req) # 3. 合并发送参数 send_kwargs { timeout: timeout, allow_redirects: allow_redirects, proxies: proxies, stream: stream, verify: verify, cert: cert, } # 4. 发送请求 resp self.send(prep, **send_kwargs) return resp关键信息有三个3.1 所有状态都挂在 Session 上Session对象初始化时会创建headers统一的默认请求头cookies可自动保存的 CookieJarauth默认认证信息adapters不同协议对应的传输适配器。也就是说Session 是一个“状态容器”。你在一个 Session 上发出的所有请求都会共享这份状态。3.2 Cookie 自动保存的机制在Session.send()中Requests 会在收到响应后自动把Set-Cookie写入self.cookies。你下一次通过同一个 Session 发请求时prepare_request()会把保存在 Session 里的 cookies 合并到新请求中。这就是为什么登录接口用session.post()执行一次后后续所有带鉴权的请求都能自动带上 Cookie。3.3 Adapter 和连接池复用再看Session.send()的核心部分def send(self, request, **kwargs): # ... adapter self.get_adapter(urlrequest.url) r adapter.send(request, **kwargs) return rSession如何找到对应的 Adapter就是通过初始化时mount()的 URL 前缀。默认注册了两个http://对应HTTPAdapter()https://对应HTTPAdapter()HTTPAdapter内部维护了一个urllib3.PoolManager连接池。同一个 host 的请求会复用同一个连接池中的 TCP 连接这就是 Session 比直接多次调用requests.get()高效的原因。到这里核心结论已经很清楚接口自动化测试中应该用requests.Session()封装客户端而不是散落地调用requests.get()/requests.post()。4. 环境准备与最小工程结构下面进入实战环节。我们先准备一个可复用的接口自动化测试最小工程。4.1 环境依赖建议使用 Python 3.9 及以上版本。本文的代码不依赖某个特定版本的 Requests只要你的环境是 Requests 2.x 系列即可。创建虚拟环境并安装依赖python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate pip install requests pytest pytest-html安装完成后验证一下python -c import requests; print(requests.__version__)如果能输出版本号说明环境就绪。4.2 工程目录结构我建议按下面的结构组织测试工程api_test_project/ ├── common/ │ ├── __init__.py │ └── http_client.py # APIClient 封装 ├── test_cases/ │ ├── __init__.py │ ├── test_login.py # 登录用例 │ └── test_user.py # 业务接口用例 ├── config/ │ ├── __init__.py │ └── settings.py # 环境配置 ├── reports/ # 测试报告输出目录 └── pytest.inipytest.ini文件内容[pytest] testpaths test_cases addopts -v --tbshort这个结构虽然简单但足够支撑一个中大型接口测试项目的基本骨架。5. 接口自动化测试完整实战从登录态到业务链路假设我们正在测试一个典型的 RESTful 接口服务环境地址为http://127.0.0.1:8000。它提供两个关键接口POST /api/login登录接口入参为username、password成功返回{code: 0, data: {token: xxxx}}GET /api/user/info获取用户信息需要请求头携带Authorization: Bearer token。很多测试项目会遇到的问题是用户信息接口依赖登录接口返回的 token但登录接口又只在第一个用例中执行一次。如果每次请求都重新登录测试效率很低如果登录态不共享后面的用例又全部失败。用 Session 封装可以优雅地解决这个问题。5.1 封装 APIClient新建common/http_client.py# common/http_client.py import requests class APIClient: 接口测试客户端基于 requests.Session 封装 def __init__(self, base_url: str): self.base_url base_url.rstrip(/) self.session requests.Session() self.token None def login(self, username: str, password: str) - dict: 登录接口获取 token 并自动写入 Session 的 Header url f{self.base_url}/api/login resp self.session.post( url, json{username: username, password: password}, ) # 如果服务端返回非 2xx直接抛出异常避免用例继续往下跑 resp.raise_for_status() data resp.json() if data.get(code) ! 0: raise AssertionError(f登录失败: {data}) self.token data[data][token] # 将 token 写入统一 Header后续所有请求自动携带 self.session.headers.update({Authorization: fBearer {self.token}}) return data def get_user_info(self) - dict: 获取当前登录用户信息 url f{self.base_url}/api/user/info resp self.session.get(url) resp.raise_for_status() return resp.json()这段代码的关键设计是所有请求都通过self.session发出而不是requests.get()保证 Cookie 和连接池的复用。登录成功后统一更新 Session 的 Header后续业务接口不用再手动拼 token。对响应状态码做快速失败避免接口异常时用例继续执行造成误判。5.2 编写 pytest 测试用例新建test_cases/test_user.py# test_cases/test_user.py import pytest from common.http_client import APIClient pytest.fixture(scopemodule) def client(): 模块级 fixture整个测试模块只初始化一次客户端 api_client APIClient(base_urlhttp://127.0.0.1:8000) return api_client pytest.fixture(scopemodule) def login(client): 模块级 fixture只执行一次登录供所有用例复用登录态 client.login(admin, 123456) return client def test_login_success(client, login): 登录后不应为 None同时 token 应该被写入 Header assert client.token is not None assert client.session.headers.get(Authorization) fBearer {client.token} def test_get_user_info_authorized(login): 携带 token 请求用户信息应返回 200 和正确用户名 data login.get_user_info() assert data[code] 0 assert data[data][username] admin def test_get_user_info_unauthorized(): 不登录直接请求用户信息应被拒绝 api_client APIClient(base_urlhttp://127.0.0.1:8000) resp api_client.session.get(f{api_client.base_url}/api/user/info) assert resp.status_code 401这里有个很典型的测试设计模式“先失败、后成功”。第三个用例故意不登录模拟一个未认证请求验证服务端正确处理返回 401。很多新手只写“成功路径”的用例导致服务端鉴权失效这种严重问题完全暴露不出来。5.3 运行测试并查看结果pytest预期输出中包含类似信息test_cases/test_user.py::test_login_success PASSED test_cases/test_user.py::test_get_user_info_authorized PASSED test_cases/test_user.py::test_get_user_info_unauthorized PASSED如果登录失败或 token 未生效第一个用例就会失败后续用例也会连带失败。这种“快速失败 状态共享”的设计能帮助你在接口测试中快速定位是登录问题还是业务逻辑问题。6. 深入源码Request 是如何变成 PreparedRequest 的前面的实战代码能跑通但很多人不理解一个问题为什么我传入的json会被自动编码为请求体为什么 URL 参数会自动拼接答案在PreparedRequest中。在Session.request()中Request对象会被传给prepare_request()方法。Request本质上只是一个“数据容器”它保存了你传入的参数但还没有变成可以发送的 HTTP 请求。prepare_request()的核心逻辑如下def prepare_request(self, request): # 如果请求没有单独指定 cookies则使用 Session 里保存的 cookies cookies request.cookies or {} if not cookies: cookies self.cookies # 创建 PreparedRequest并执行 prepare 系列方法 prep PreparedRequest() prep.prepare( methodrequest.method, urlrequest.url, headersrequest.headers, filesrequest.files, datarequest.data, jsonrequest.json, paramsrequest.params, authrequest.auth, cookiescookies, hooksrequest.hooks, ) return prep而PreparedRequest.prepare()内部会按顺序调用多个prepare_xxx方法主要步骤包括方法职责prepare_method将请求方法转为大写如get→GETprepare_url解析和校验 URL处理 URL 中的非 ASCII 字符prepare_headers合并默认 Header 和用户传入 Headerprepare_body根据data、json、files生成请求体并设置 Content-Typeprepare_auth处理 Basic Auth、Bearer Token 等认证信息prepare_cookies将 Cookie 写入请求头prepare_hooks注册钩子函数举个例子。你调用session.post(http://127.0.0.1:8000/api/login, json{username: admin})在prepare_body()阶段Requests 会检查到json参数不为空于是调用complexjson.dumps()把字典序列化成 JSON 字符串设置Content-Type: application/json请求头把序列化后的字符串写入请求体。这也是为什么很多新手分不清data和json的区别data传字符串或者字典Requests 会把它当作表单格式进行编码json传字典Requests 会自动序列化为 JSON 字符串并设置Content-Type: application/json。接口测试中到底用data还是json取决于后端接口到底接收 form 格式还是 JSON 格式。如果你把 JSON 数据用data传过去后端用RequestBody接就会直接报参数缺失。反过来表单类型的接口你用json传后端用RequestParam或者request.form接同样拿不到数据。7. 接口测试高频问题与排查思路接口测试做得越多你遇到的神奇问题就越多。这里把 Requests 使用中最容易踩的坑整理成了一张排查表问题现象可能原因排查方式解决方案接口返回 401 / 403登录态未保存或 token 未传打印发起请求的 headers 和 cookies改用 Session登录后将 token 写入统一 Header429 Too Many Requests超过了服务端限流阈值查看响应头的Retry-After字段配置重试策略增加退避时间Max retries exceededURL 写错、服务端拒绝连接、网络不通用curl手动请求确认接口可达检查 URL、代理设置、DNS确认服务是否启动SSL 证书校验失败测试环境使用自签名证书查看完整报错信息测试环境可临时设置verifyFalse生产环境不建议响应中文乱码请求头或响应头缺少 charset打印resp.encoding手动指定resp.encoding utf-8连接池报错并发量超过连接池默认上限查看异常栈是否包含urllib3.connectionpool调大HTTPAdapter的pool_maxsize脚本执行时间过长未设置 timeout请求挂起检查是否有请求长时间未返回所有请求都设置timeout7.1 重点429 限流的正确重试姿势接口测试中429 是非常常见的状态码尤其是被测服务没有做好限流或者你跑的用例太密集时。很多人遇到 429 的第一反应是“等一会儿再跑”但人工等待不适用于自动化测试。正确做法是在客户端配置重试策略。注意重试逻辑不在 Requests 层而是在 urllib3 层通过HTTPAdapter传入# common/http_client.py重试策略增强版 import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def create_session_with_retry( retry_times: int 3, backoff_factor: float 1.0, status_forcelist: tuple (429, 500, 502, 503, 504), ) - requests.Session: 创建带重试策略的 Session backoff_factor1.0 表示第一次重试等待 1 秒第二次 2 秒第三次 4 秒 retry_strategy Retry( totalretry_times, backoff_factorbackoff_factor, status_forceliststatus_forcelist, # 不同 urllib3 版本参数名可能不同allowed_methods / method_whitelist allowed_methods[GET, POST, PUT, DELETE, HEAD, OPTIONS], raise_on_statusFalse, ) adapter HTTPAdapter(max_retriesretry_strategy) session requests.Session() session.mount(http://, adapter) session.mount(https://, adapter) return sessionbackoff_factor的机制值得解释一下它是 urllib3 的重试退避因子。计算公式是sleep_time backoff_factor * (2 ** (重试次数 - 1))也就是backoff_factor1时第一次重试前等待 1 秒第二次等待 2 秒第三次等待 4 秒。这种指数退避策略可以显著降低对服务端的瞬时压力比固定间隔重试更可靠。不过要注意不是所有接口都适合自动重试。只有幂等接口——比如查询类接口——才能放心重试。如果是一个创建订单的 POST 接口重试可能导致订单重复创建。稳妥的做法是只在 GET 请求上配置这种全局重试写操作接口单独处理。7.2 重点连接池扩容Requests 默认连接池很小HTTPAdapter的默认参数是pool_connections10、pool_maxsize10。如果是做并发测试或者测试脚本是多线程发请求的很容易出现连接池不够用的情况。可以通过调整 Adapter 参数解决adapter HTTPAdapter( pool_connections20, pool_maxsize50, max_retriesretry_strategy, ) session.mount(https://, adapter)pool_connections表示缓存的连接池数量pool_maxsize表示每个连接池最大连接数。具体数值要根据你测试的并发规模来定但这几个参数的含义必须清楚。8. 接口自动化测试最佳实践工程化落地建议最后一章聊一些接口自动化测试工程化的建议。这些都是从实际项目中总结出来的不会写进官方文档里。8.1 永远封装 APIClient不要散写请求最差的接口测试代码是每个用例里都写几十行requests.post()参数、Header、断言全部挤在一起。这种代码维护成本极高接口一变更所有用例都要改。正确做法是把接口请求封装到一个APIClient类里每个接口对应一个方法。用例层面只关心“入参”和“期望结果”不关心 HTTP 细节。8.2 环境配置和敏感信息要分离不要把测试环境的地址、账号密码直接硬编码在测试代码里。至少做成配置文件# config/settings.py import os BASE_URL os.getenv(BASE_URL, http://127.0.0.1:8000) USERNAME os.getenv(TEST_USERNAME, admin) PASSWORD os.getenv(TEST_PASSWORD, 123456)生产环境的凭据信息应该通过 CI 的 Secret 变量注入而不是提交到代码仓库。8.3 严格区分测试环境与生产环境的安全边界接口测试一般针对测试环境或预发布环境。如果是在生产环境做只读验证必须使用最小权限账号并且只允许执行 GET 等只读请求。任何涉及删除、批量修改、资金操作的接口都不应该在无人审批的情况下自动执行。8.4 断言要结构化不要只判断状态码新手写断言最喜欢写assert resp.status_code 200但状态码 200 只代表 HTTP 请求成功不能代表业务成功。很多系统在业务异常时也会返回 HTTP 200只是业务码变了。更稳的断言思路是先判断 HTTP 状态码再判断业务码如code 0再校验关键业务字段如token不为空、username正确必要的时候校验响应时间防止接口性能劣化。8.5 测试数据要做到可重复执行接口测试最怕“反复执行结果不一致”。所以测试数据设计非常重要登录账号尽量用专用测试账号不要和其他环境共用写入类操作尽量使用随机数据或唯一标识避免数据冲突测试结束后做数据清理避免垃圾数据累积。你可以用 UUID 或者时间戳生成唯一用户名import time username ftest_{int(time.time())}_{uuid.uuid4().hex[:8]}8.6 善用 pytest fixture 管理资源上一章实战代码里的 fixture 就是很好的示范clientfixture 负责初始化 APIClientloginfixture 负责完成登录不同用例可以灵活声明依赖哪个 fixture做到登录态复用和用例隔离的平衡。如果某个用例确实需要独立的登录态就单独创建一个 fixture而不要污染全局的登录 fixture。8.7 日志和报告是排查问题的第一抓手接口自动化测试一旦失败最重要的不是看断言代码而是看当时的请求报文和响应报文。所以封装 APIClient 时可以考虑加日志输出import logging logger logging.getLogger(api_test) def _request_with_log(self, method, url, **kwargs): logger.info(f请求: {method} {url}) logger.info(f请求参数: {kwargs}) resp self.session.request(method, url, **kwargs) logger.info(f响应状态码: {resp.status_code}) logger.info(f响应内容: {resp.text[:500]}) return resp有了请求日志和响应日志排查问题就能少花一半时间。8.8 指定 timeout永远不要让请求无限挂起接口测试里最可怕的不是“很快失败”而是“一直不返回”。一个没有设置 timeout 的请求可能让整个测试任务挂在那里白白占用 CI 资源。resp self.session.post(url, jsondata, timeout10)这里的timeout10表示连接超时和读取超时都是 10 秒。你甚至可以单独设置timeout(3.05, 15)第一个值是连接超时第二个值是读取超时。每次请求都显式设置 timeout应该成为接口测试的强制规范。9. 总结与下一步学习方向这篇文章从 Requests 源码的调用链讲起落到接口自动化测试的真实工程实践。核心信息可以浓缩成四句话Requests 本质是状态管理和请求装配层真正发请求的是底层的 urllib3接口自动化测试要用 Session不要散落调用 requests.get() / post()这是登录态和连接池正确复用的基础重试策略、连接池大小都在 Adapter 层配置在 Requests 顶层找不到答案接口测试工程的稳定性靠的是封装、断言、日志、数据隔离和 timeout 规范而不是某一行魔法代码。读完这篇文章建议你接下来做三件事第一打开你本地的 Requests 安装目录找到requests/sessions.py和requests/adapters.py顺着文章里的调用链把Session.request()、Session.send()、HTTPAdapter.send()这三个方法完整读一遍。第二把文章里的APIClient封装落地到你自己的测试项目里加上日志和 timeout 规范。第三深入读一读urllib3.util.retry.Retry的源码理解指数退避、重试状态码、重试方法这些参数的完整含义。Requests 是一个很适合“手撕源码”的库因为它足够小、足够经典、足够常用。把这一层吃透你的接口自动化测试能力会上一个台阶也为你后续去理解 pytest 框架、HTTPX 库、以及各类自动化测试平台打下基础。建议收藏备用下次在接口测试里遇到诡异问题不妨回来翻翻这篇文章的调用链图和排查表。