用 autospec 与 spec_set 让 unittest.mock 严格匹配真实接口
问题场景:Mock 太宽容,测试会假通过
用 unittest.mock.Mock 替换依赖对象时,Mock 不会检查真实对象上是否存在被访问的属性,也不会检查方法签名。你写 client.send(...),它返回一个子 Mock;你误写成 client.sendd(...),它同样返回一个子 Mock。调用不报错,断言也可能通过,但真实对象根本没有 sendd,上线后才会暴露 AttributeError。
更隐蔽的是签名错误。真实方法要求 send(to, subject),测试里只传了 to,默认 Mock 依然接受。测试无法发现调用方少传参数。
复现假通过
demo_app.py:
class EmailClient:
def send(self, to, subject):
raise NotImplementedError
class UserService:
def __init__(self, email_client):
self.email_client = email_client
def register(self, email):
self.email_client.send(to=email, subject='欢迎注册')
class BrokenUserService(UserService):
def register(self, email):
# 故意写错:真实 EmailClient 没有 sendd
self.email_client.sendd(to=email, subject='欢迎注册')
test_demo.py:
from unittest.mock import Mock, create_autospec
import pytest
from demo_app import EmailClient, BrokenUserService
def test_typo_passes_with_plain_mock():
client = Mock()
service = BrokenUserService(client)
service.register('user@example.com')
# 测试通过了,但真实对象没有 sendd
client.sendd.assert_called_once_with(
to='user@example.com', subject='欢迎注册'
)
运行 python -m pytest -q,这个测试会通过。原因是 Mock 自动为 sendd 创建了子 Mock。
用 autospec 与 spec_set 收紧约束
create_autospec 可以基于一个真实对象或类创建 mock,使可用属性和方法签名与真实对象一致。spec_set=True 进一步禁止访问或设置不在规格中的属性。
修复后的测试:
def test_typo_fails_with_autospec():
# instance=True 表示 mock 是 EmailClient 的实例形态
client = create_autospec(EmailClient, instance=True, spec_set=True)
service = BrokenUserService(client)
with pytest.raises(AttributeError):
service.register('user@example.com')
此时 client.sendd 不在 EmailClient 的规格中,访问会抛 AttributeError,测试不会假通过。
签名检查示例:
def test_signature_is_checked():
client = create_autospec(EmailClient, instance=True, spec_set=True)
# 缺少 subject,autospec 会抛 TypeError
with pytest.raises(TypeError):
client.send(to='user@example.com')
如果使用 patch,可以写:
from unittest.mock import patch
@patch.object(EmailClient, 'send', autospec=True, spec_set=True)
def test_patch_autospec(mock_send):
service = UserService(EmailClient())
service.register('user@example.com')
mock_send.assert_called_once_with(
service.email_client,
to='user@example.com',
subject='欢迎注册',
)
注意:patch.object 对实例方法的 autospec 会把 self 作为第一个参数传入,所以断言里需要包含实例。这是常见细节。
运行方法
- 安装 pytest:
pip install pytest。 - 将两个文件放在同一目录。
- 运行:
python -m pytest -q。
预期:test_typo_passes_with_plain_mock、test_typo_fails_with_autospec、test_signature_is_checked 通过。若把 spec_set=True 去掉,第二个测试可能不再按预期失败,说明约束变弱。
边界与限制
autospec主要检查方法和函数的参数数量与名称,不检查参数类型。类型错误仍可能漏过。- 对动态属性(如通过
__getattr__生成的属性)或 C 扩展、内置类型,autospec可能拿不到完整规格,需要显式传入spec或改用更具体的测试替身。 spec_set=True会禁止设置不存在的属性。如果真实对象允许动态添加属性,这种严格模式可能过于限制,需要按场景权衡。autospec创建的 mock,其return_value不一定自动拥有完整规格。若返回值也要严格,可继续用create_autospec构造,或手动设置带规格的 mock。- 异步方法需要确认当前 Python 运行环境的
unittest.mock是否自动生成AsyncMock;若不支持,可显式使用AsyncMock。 - 严格规格能减少假通过,但不能替代对真实行为的集成测试。mock 验证的是交互契约,不是真实实现。
总结:在单元测试中,优先用 create_autospec(..., spec_set=True) 或 patch(..., autospec=True, spec_set=True) 替换依赖。它能让拼写错误、签名不匹配、误设属性尽早暴露,避免测试通过而线上失败。
- 点赞
- 收藏
- 关注作者
评论(0)