Python 自动化测试:代理IP 环境变量污染如何验证
同一份测试在开发机能过、到 CI 就失败,常常是残留的代理变量在捣乱。文章列出这些变量及其读取顺序,用 fixture 固定环境、请求前断言、回显出口 IP 证明链路,再给出清理检查与故意失败自检。
同一份测试,在开发机上通过、在 CI 上失败,或者反过来——十有八九是环境里的代理变量在起作用。requests、urllib、httpx 等常见实现通常会读取环境变量,具体行为随库和版本而变;CI 镜像又常预置 HTTP_PROXY。判断"有没有被污染"不能靠肉眼扫代码,要靠在测试内部固定环境并断言出口。这些变量指向的代理IP,往往和用例约定的不是同一条。NO_PROXY 规则本身的行为验证属于另一个话题,本篇讲的是测试框架里的环境隔离。
代理环境变量污染通常会表现为什么
- 意外走代理:本应直连的请求,被环境变量送进了代理,出口 IP 与预期不符。
- 意外绕过代理:本应走代理的请求,被
NO_PROXY或空值放行,直接连到了目标。
两种表现都会让断言"通过得莫名其妙"。前者常出现在本地缓存或 mock 场景,后者常出现在 CI 里变量拼写错误或作用域不对的场景。判定污染不看代码写了什么,看实际请求从哪里出去。
哪些代理环境变量会生效,优先级如何判断
变量不是设了就生效,读取顺序、大小写和库的默认值都会改变结果;变量只描述地址和协议,不描述代理IP的身份,身份得另做出口校验。把"默认"当成待实测的假设:按库和版本分别验证,并补一个故意设错变量的负向用例。
为什么要同时检查大小写变量和 ALL_PROXY
- 变量:
HTTP_PROXY/http_proxy| 作用范围:http 请求 | Python 客户端是否读取:requests、urllib 默认读取 | 测试中如何处理:fixture 内显式设置或清空 - 变量:
HTTPS_PROXY/https_proxy| 作用范围:https 请求 | Python 客户端是否读取:同上 | 测试中如何处理:同上 - 变量:
ALL_PROXY/all_proxy| 作用范围:未单独指定协议时 | Python 客户端是否读取:requests 读取 | 测试中如何处理:容易遗漏,需一并清理 - 变量:
NO_PROXY/no_proxy| 作用范围:排除列表 | Python 客户端是否读取:requests、urllib 读取 | 测试中如何处理:边界用例单独覆盖
ALL_PROXY 最容易被漏:未单独指定协议时它接管全部请求,清理时不能只清 HTTP_PROXY 与 HTTPS_PROXY。
环境变量大小写相同时应该相信哪一个
同名不同大小写时,取值顺序可能相反。requests 通过 urllib.request.getproxies() 取值,常见实现小写优先,但版本和平台不同结果会变,也有库只认大写。测试里不要依赖,统一由 fixture 显式设定,并在请求前断言生效值,再用实际出口校验。
为什么没写代理的 requests 仍可能走代理
Python 的 requests 常见版本默认 trust_env=True,会读取环境配置;该开关还会影响 .netrc 等其他来源。需要隔离环境代理时,优先为测试创建专用 Session 并设置 trust_env=False,再显式配置测试所需的认证与 CA。这样也会关闭该 Session 的其他环境来源,需按实际需求补配。不要把只对 http、https 设置 None 当成覆盖所有代理环境变量的通用方法;应按 requests 版本、目标地址和实际出口验证。
代理环境变量与请求库的关系可对照代理设置帮助。
如何用 fixture 固定环境并提前断言
在测试层做三件事:
1. autouse fixture 在用例开始前清空所有代理变量(大小写各一份),用例结束后恢复。
2. 需要代理的用例通过参数显式注入,不依赖外部环境。
3. 请求发出前断言:os.environ 中的值、session.proxies、session.trust_env 与预期一致。
```python
import pytest
@pytest.fixture(autouse=True)
def clean_proxy_env(monkeypatch):
for key in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY",
"NO_PROXY", "http_proxy", "https_proxy", "all_proxy",
"no_proxy"):
monkeypatch.delenv(key, raising=False)
```
需要测试 NO_PROXY 时,在对应参数化用例中单独设置目标值。monkeypatch 会在用例结束时恢复原环境;可加一个检查,确认所有大小写变量恢复到用例前状态。详情见 pytest monkeypatch 文档。
NO_PROXY 哪些边界情况必须单独测试
NO_PROXY 的匹配规则容易想当然,至少要覆盖:
- 精确域名与子域名:
example.com是否覆盖api.example.com。 - 端口写法:
example.com:443与不带端口的差异。 - IP 与 CIDR:单 IP、网段写法是否被支持。
- 本机地址:
localhost、127.0.0.1、::1。 - 通配:
*是否让所有请求直连。 - 空值:变量存在但为空字符串时的行为。
每个边界写成一个用例,断言"是否走了代理",而不是"变量是否被读取"。变量被读取但匹配失败,同样属于污染。
如何证明请求真的走了预期出口
环境变量只是配置,出口才是结果。回显请求必须由同一个被测客户端、协议和目标匹配规则发出;另用受控目标的访问日志或代理到达记录核对真正的业务请求。不同目标可能命中不同的 NO_PROXY 规则,单独请求另一个回显域名不能证明被测请求走了同一路径。直连出口应现场测量,并注明 NAT 或 CI 共享出口的影响:
- 期望走代理:出口应属于代理侧地址段。
- 期望直连:出口应属于本机公网地址段。
- 期望绕过:目标应命中
NO_PROXY,出口为直连地址。
回显服务用自建或受控端点。分别覆盖预期走代理、命中绕过规则、故意设置错误配置和恢复后的请求,并记录目标、时间、客户端版本与代理到达证据。代理 URL 只记录脱敏后的协议、主机、端口和配置来源;删除 userinfo、令牌及敏感查询参数,避免 CI 日志泄露凭据。
如何隔离开发机和 CI 继承的代理环境
本地 shell 里的代理变量会通过 pytest 进程继承进来,CI 的容器镜像也可能预置。隔离手段:
- 在
pytest.ini或conftest.py里统一清理,不依赖开发者本地配置。 - CI 任务显式声明环境,不继承 runner 的全局变量。
- 需要真实代理的集成测试单独标记,默认不跑。
区分"单元测试"与"集成测试"的边界很重要:前者不应依赖任何外部出口,后者才需要真实代理。
requests 的环境设置行为见 Requests 高级用法文档;测试环境变量恢复可参考 pytest monkeypatch 文档。
如何通过清理检查和故意失败验证测试有效
最后一步是验证测试本身有效:把预期出口故意写错,测试必须失败。如果仍然通过,说明断言没有真正落到出口上,前面所有隔离都只是形式。
清理检查同样要做:用例结束后确认 os.environ 已恢复,没有把变量泄漏给后续用例。多个用例串行时,泄漏会表现为"只有第一个用例正常"。
FAQ
Q:代码里没写代理,还会被环境变量影响吗?
A:会。requests 等常见实现默认读取环境变量,trust_env 为 True 时自动生效;具体行为以实际版本为准,测试里要显式清理或关闭,并回显出口验证。
Q:NO_PROXY 写了域名,为什么子域名还是走了代理?
A:不同实现对子域名的匹配规则不同,需要按库的实际行为写用例验证,不能假设后缀匹配一定生效。
Q:CI 上通过、本地失败,先查什么?
A:先比对变量是否存在、脱敏后的协议/主机/端口和配置来源,再用同一被测请求的目标日志或代理到达记录确认路径。不要打印完整代理 URL,其中可能含账号、密码或令牌。
