Skip to content

用 requests 写接口测试:参数编码、data 与 json、超时与会话 ​

一个接口测试脚本跑了半年,一直是绿的。有一天接口真的坏了,返回 500,脚本还是绿的;另一天测试环境的网关卡住,整个 CI 流水线挂了四个小时,最后被人手动取消。

Python 的 requests 是写接口测试最常用的库之一,用起来非常简单,几行代码就能发出请求、拿到 JSON。也正因为简单,它的几个默认行为很容易被忽略:查询参数要不要自己拼、data 和 json 有什么区别、请求会不会超时、500 会不会抛异常、Cookie 和连接会不会复用。每一个都可能让测试「通过」了一个本该失败的情况,或者让测试本身变得不可靠。

本文用 Python 3.14 和 requests 2.34.2,在一个本地回显服务上把这几个默认行为逐个跑一遍。实验见文末。

默认 / 常见写法应当查询参数拼 ?q=a b&c:q='a b',多出 cparams=:q=a+b%26c请求体data=dict 是表单;嵌套结构丢失json=:application/json超时不设:3 秒的接口等了 3 秒timeout=1:1 秒后 ReadTimeout状态码500 不抛异常,resp.ok=False断言具体状态码 / raise_for_status会话登录后 /me 401;7 次请求 7 个连接Session:200;7 次请求 1 个连接测试客户端把这些固定下来:默认超时、必填的期望状态码、共享会话,失败信息带上方法、路径、状态码和响应体开头
图 1 · 实测(Python 3.14.8、requests 2.34.2):自己拼查询串时 & 把参数拆错了;data=dict 发出去的是表单,嵌套结构直接丢失;不设 timeout 就一直等;500 响应不抛异常,只检查响应体的测试照样通过;不用 Session 时登录状态不保留,7 次请求开了 7 个连接。

一、查询参数:交给 params,不要自己拼 ​

text
自己拼 URL「?q=a b&c」:服务端收到 {'q': ['a b'], 'c': [''], 'tag': ['x']}
params={'q': 'a b&c', 'tag': ['x', 'y']}:查询串 q=a+b%26c&tag=x&tag=y,服务端收到 {'q': ['a b&c'], 'tag': ['x', 'y']}

用 f-string 把参数拼进 URL,值里的 & 被当成了参数分隔符,q 只剩下 a b,还多出一个空的参数 c。中文、+、#、= 都有类似的问题。交给 params,requests 会按规则编码;列表会展开成重复的参数(tag=x&tag=y),这也是大多数服务端框架期望的格式。

测试数据里特意放上这些字符是值得的:搜索关键词、用户名、备注里迟早会出现 & 和空格,测试应该比用户先遇到它们。

二、请求体:data 是表单,json 才是 JSON ​

text
data=dict:Content-Type application/x-www-form-urlencoded,请求体 sku=A1&qty=2
json=dict:Content-Type application/json,请求体 {"sku": "A1", "qty": 2}
data=json.dumps(...):Content-Type None,请求体 {"sku": "A1", "qty": 2}
data 里放嵌套结构:请求体 items=sku&items=sku

data= 传字典时,requests 按表单编码;只有 json= 才会序列化成 JSON 并设置 Content-Type: application/json。把 json.dumps 的结果交给 data=,请求体看起来对了,但没有 Content-Type,按 Content-Type 选择解析方式的服务端不会把它当 JSON。嵌套结构交给 data= 更糟:表单编码无法表达嵌套,内容被压扁成了 items=sku&items=sku,原来的数据全丢了。

规则很简单:接口收 JSON,就用 json=。

三、超时:默认不设 ​

text
不设 timeout 请求一个 3 秒才响应的接口:等了约 3 秒;requests 默认不设超时,对方一直不响应就一直等
timeout=1:约 1 秒后抛出 ReadTimeout

requests 默认没有超时。对方接受了连接但一直不响应(网关卡住、服务死锁),请求就一直挂着,测试进程也一直挂着,直到 CI 的全局超时把整个流水线杀掉,失败报告里只有「超时」,不知道是哪个请求。

每个请求都要设置 timeout,可以是一个数,也可以是 (连接超时, 读取超时) 的二元组。在测试框架里,最简单的做法是封装一个带默认超时的会话,所有请求都通过它发出。超时该设多少、重试怎样与超时配合,见每层都重试三次,最底下的服务收到二十七倍请求。

四、状态码:500 不抛异常 ​

text
500 响应:不抛异常,resp.ok=False;只断言 resp.json() 有内容的测试照样通过
raise_for_status():HTTPError:500 Server Error: Internal Server Error

requests 把任何收到的响应都当作成功:4xx、5xx 都正常返回。如果测试只检查响应体里有某个字段,而错误响应也是 JSON({"error": "boom"}),断言「响应是一个字典」「有内容」就会照样通过。开头那个一直是绿的脚本就是这样。

每个测试都要明确断言状态码:期望成功时断言 200(或 raise_for_status()),期望失败时断言具体的错误码,例如 409、422。只断言「不是 500」也不够,404 一样不是你要的结果。Postman 脚本里同样的问题,见 Postman 自动管理 Token:脚本能跑通,不代表失败时会报错。

text
不用 Session:先登录再访问 /me,状态 401
Session:登录后访问 /me,状态 200;7 次请求用了 1 个 TCP 连接
不用 Session 发 7 次请求:用了 7 个 TCP 连接

requests.get 每次调用都是独立的:登录返回的 Cookie 不会带到下一个请求,连接也不复用。requests.Session 会保存 Cookie、复用连接,还可以统一设置请求头和认证信息。

需要多个步骤的场景测试(登录、下单、查询)应该使用会话,否则要在每一步手动传递 Cookie。连接复用对测试的速度也有明显影响,特别是 HTTPS 下每个新连接都要握手。反过来,需要验证「不带凭证时被拒绝」的测试,要用一个新的会话,不能复用已经登录的那个。

测试准备的数据怎样区分静态参数、运行时状态和环境配置,见从行为到场景到断言;单接口与跨步骤测试的分工,见这个风险该由哪一层测试发现。

六、一个最小的测试客户端 ​

把上面的要点放在一起:

python
class ApiClient:
    def __init__(self, base_url, timeout=(3, 10)):
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.session = requests.Session()

    def request(self, method, path, expected_status, **kwargs):
        kwargs.setdefault("timeout", self.timeout)
        resp = self.session.request(method, self.base_url + path, **kwargs)
        assert resp.status_code == expected_status, f"{method} {path}: {resp.status_code} {resp.text[:200]}"
        return resp

expected_status 是必填参数,测试必须说清期望的状态码;失败信息带上方法、路径、状态码和响应体的开头,在 CI 报告里就能看出是哪个请求出了问题。

七、检查清单 ​

  • 查询参数交给 params,测试数据里包含 &、空格、中文。
  • 接口收 JSON 就用 json=;不要把 json.dumps 交给 data=。
  • 每个请求都设置 timeout。
  • 每个测试都断言具体的状态码。
  • 多步骤场景用 Session;验证未认证的测试用新的会话。
  • 失败信息包含方法、路径、状态码和响应体开头。

配套实验

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权