HTTPX MockTransport 的响应已有正文,访问 elapsed 仍然报错
HTTPX 0.28.1 的手工模拟响应可能已有正文却没有计时字段。在测试夹具显式设置 timedelta,保留真实流读取和关闭路径的验证。
测试里的 MockTransport 返回了一个带 JSON 正文的 httpx.Response,response.json() 正常,读取 response.elapsed 却报:
RuntimeError: '.elapsed' may only be accessed after the response has been read or closed.
在 HTTPX 0.28.1 中,手工创建的响应不一定经历真实请求的计时与关闭路径。遇到模拟传输时,不能只根据这句错误再调用一次 read() 或 close(),就期待它自动补出耗时。
正文就绪与耗时存在是两件事
Response(200, json=...) 可以在构造时读入正文并标记关闭,此时尚未交给客户端的计时包装。MockTransport 的处理方法只是调用 handler,返回它提供的 Response,并不主动给这个对象设置 elapsed。
客户端正常路径会在绑定的响应流关闭时记录耗时。但一个构造时已经关闭、已有 _content 的响应,随后再次读取或关闭,可能不会触发那个计时包装的关闭方法。因此可以出现正文可用、对象也已关闭,却没有 _elapsed 的组合。
先确认响应是否来自测试 handler,堆栈是否真的停在 elapsed 属性。真实网络流尚未读完,是另一种符合这句错误提示的情况,应按原流式响应生命周期消费或关闭,不能用模拟耗时掩盖资源未释放。
在测试夹具里明确给出耗时
如果测试只需要一个确定的耗时值,应在构造模拟响应的位置显式设置。下面是同步客户端的独立夹具示例,所有请求由内存传输处理:
from datetime import timedelta
import httpx
def reply(request):
response = httpx.Response(200, json={"ok": True})
response.elapsed = timedelta(milliseconds=25)
return response
with httpx.Client(transport=httpx.MockTransport(reply)) as client:
response = client.get("https://example.invalid/check")
elapsed_seconds = response.elapsed.total_seconds()
25 毫秒是这个夹具人为指定的值,不能写进性能报告,或当成 MockTransport 实际花费的时间。测试应围绕它检查业务代码的单位换算、阈值分支或展示逻辑,而不是证明外部服务延迟。
多个 handler 都需要这一字段时,可以在项目自己的测试工厂中集中设置。不要改 HTTPX 的私有 _elapsed,公共 elapsed setter 已接受 timedelta;也不要为了少写一行,给所有真实响应统一补零,掩盖未完成读取或计时失败。
如果测试目标正是计时器行为,应使用能覆盖预期流生命周期的受控传输,并明确计时范围;固定字段无法验证真实 DNS、TLS、连接池等待或网络耗时。普通 Response 的 JSON 正文正确,也不能证明那些路径被测试过。
保留真实流式响应的检查
调整夹具后,检查模拟结果携带的值与预设相同,并让所有原业务断言继续运行。另保留真实或受控流式响应的读取、异常与关闭测试,避免测试工厂把尚未完成读取的响应也标记成已计时。
这项处理只改变测试对象的元数据,不修复真实网络性能。恢复旧夹具时也应一并恢复对应的耗时断言,不能留下依赖固定 25 毫秒的业务逻辑,或把模拟字段带入生产监控。


