用 pytest-socket 禁用网络访问:在 pytest.ini 中添加 --disable-socket
用 pytest-socket 禁用网络访问:在 pytest.ini 中添加 --disable-socket
问题场景
单元测试应当快速、稳定、可重复。但有些代码在测试中会意外触发真实 HTTP 请求:例如忘记替换客户端、依赖初始化时探测远程配置、缓存未命中时回源。这类测试依赖外部服务可用性、网络延迟和远端数据,容易在本地通过、在 CI 失败,或者污染真实数据。即使测试当前通过,也可能在无网络环境下随机失败。
一种低成本护栏是:让测试进程默认无法建立网络连接。pytest-socket 提供了 --disable-socket 选项,可以在 pytest.ini 中全局开启,使意外网络访问立即失败,从而暴露未打桩的依赖。
安装与最小配置
安装:
pip install pytest pytest-socket
在项目根目录的 pytest.ini 中:
[pytest]
addopts = --disable-socket
这样运行 pytest 时默认禁用 socket 连接。若已有 addopts,可合并,例如 addopts = -q --disable-socket。
可运行示例
假设被测模块会调用 HTTP 接口:
# app.py
import json
import urllib.request
def get_user_name(base_url: str, user_id: str) -> str:
url = f'{base_url}/users/{user_id}'
with urllib.request.urlopen(url, timeout=3) as resp:
data = json.loads(resp.read().decode('utf-8'))
return data['name']
单元测试中用 monkeypatch 替换 urllib.request.urlopen,避免真实网络:
# test_app.py
import json
from app import get_user_name
class FakeResponse:
def __init__(self, payload: bytes):
self.payload = payload
def read(self) -> bytes:
return self.payload
def __enter__(self):
return self
def __exit__(self, *args):
return False
def test_get_user_name(monkeypatch):
payload = json.dumps({'name': 'alice'}).encode('utf-8')
def fake_urlopen(url, timeout=3):
assert url == 'http://api.example.invalid/users/123'
assert timeout == 3
return FakeResponse(payload)
monkeypatch.setattr('app.urllib.request.urlopen', fake_urlopen)
assert get_user_name('http://api.example.invalid', '123') == 'alice'
再增加一个护栏测试,确认禁用生效:
# test_guard.py
import socket
import pytest
import pytest_socket
def test_socket_is_disabled():
with pytest.raises(pytest_socket.SocketBlockedError):
socket.create_connection(('127.0.0.1', 9), timeout=0.1)
运行:
python -m pytest -q
预期两个测试通过。如果删除 monkeypatch 逻辑,让 test_app 直接调用真实 URL,测试会因 SocketBlockedError 失败。这正是隔离护栏的作用:把“意外访问网络”从难以察觉的偶发问题,变成明确失败。
边界与限制
--disable-socket 主要拦截 Python socket 层。它不能替代进程级或容器级网络隔离,也不能阻止子进程、C 扩展直接发起系统调用、已经建立的连接,或非 socket 的进程间通信。因此,它适合作为单元测试护栏,不适合作为安全边界。
若确实需要访问本地服务,应尽量缩小放行范围,例如使用 --allow-hosts=127.0.0.1,localhost,或在单个测试上使用 @pytest.mark.enable_socket。但放行后测试不再完全隔离,应明确标记为集成测试,并与默认单元测试分开运行。
异步 HTTP 客户端最终通常仍经过 socket,因此一般会被拦截,但异常可能被客户端包装,断言时建议针对业务行为而不是异常链。不同 pytest 与 pytest-socket 版本对选项、异常类的支持可能不同,建议在 CI 中固定依赖版本,并用 pytest --help 确认可用选项。
最后,禁用网络只是护栏,不是 mock 的替代。外部 HTTP 客户端、数据库、消息队列等仍需在测试中替换为受控的假实现或测试替身。
小结
在 pytest.ini 中加入 addopts = --disable-socket,可以用极低配置成本防止单元测试意外访问真实 HTTP 接口,提升测试隔离和可重复性。配合 monkeypatch 等打桩手段,并合理处理本地服务放行与集成测试边界,可以让测试套件更稳定、更可信。
- 点赞
- 收藏
- 关注作者
评论(0)