如何测试不同命名空间导入的函数未被调用?
统一拦截同一函数的不同导入调用问题
示例代码结构
foo.py
def my_func(): print('hello world!')
bar.py
from foo import my_func
初始测试问题
想要编写测试确保my_func从未被调用,编写了如下测试代码:
from unittest import mock import foo import bar @mock.patch.object(foo, 'my_func', wraps=foo.my_func) class TestMyFunc: def test_from_foo(self, wrapped_my_func): foo.my_func() wrapped_my_func.assert_not_called() def test_from_bar(self, wrapped_my_func): bar.my_func() wrapped_my_func.assert_not_called()
预期两个测试均失败,但用pytest运行时test_from_bar却通过了。原因是mock基于命名空间工作,无法识别foo.my_func和bar.my_func是同一函数对象——bar在导入时已经缓存了原函数的引用,补丁只修改了foo命名空间下的函数,不影响bar中已有的引用。
尝试改进但仍无效的方案
结合mock与monkeypatch修改测试后,test_from_bar仍通过:
from unittest.mock import MagicMock import pytest import foo import bar class TestMyFunc: @pytest.fixture def mock_my_func(self, monkeypatch): _mock = MagicMock(wraps=foo.my_func) monkeypatch.setattr(foo, foo.my_func.__name__, _mock) return _mock def test_from_foo(self, mock_my_func): foo.my_func() mock_my_func.assert_not_called() def test_from_bar(self, mock_my_func): bar.my_func() mock_my_func.assert_not_called()
问题本质还是一样:bar模块加载时已经保存了原my_func的引用,后续修改foo中的函数不会改变bar里的缓存。
实际场景需求
测试使用dask的函数时,需要确保dask.base.compute未被意外调用,但它可能以dask.compute、dask.array.compute等多种别名形式被导入和调用,不想为每个导入路径单独编写补丁和断言。
解决方案:直接修改原函数对象
所有导入的同一函数本质是同一个对象,因此可以直接修改这个函数对象的行为,让所有引用它的地方都被统一跟踪,无需逐个打补丁。
方法1:替换原函数的__call__方法
给原函数对象包装一个mock的调用逻辑,所有引用该函数的地方调用时都会触发这个mock:
from unittest.mock import MagicMock import pytest import foo import bar @pytest.fixture(autouse=True) def track_my_func_calls(): original_func = foo.my_func mock = MagicMock(wraps=original_func) # 替换原函数的__call__方法,统一跟踪调用 foo.my_func.__call__ = mock.__call__ yield mock # 测试结束后恢复原函数 foo.my_func.__call__ = original_func.__call__ class TestMyFunc: def test_from_foo(self, track_my_func_calls): foo.my_func() track_my_func_calls.assert_not_called() # 会失败,符合预期 def test_from_bar(self, track_my_func_calls): bar.my_func() track_my_func_calls.assert_not_called() # 会失败,符合预期
针对Dask场景的适配
对于dask.base.compute的拦截,同样可以用这个思路,不管是dask.compute还是dask.array.compute,都会被统一跟踪:
from unittest.mock import MagicMock import pytest import dask.base @pytest.fixture(autouse=True) def track_compute_calls(): original_compute = dask.base.compute mock = MagicMock(wraps=original_compute) dask.base.compute.__call__ = mock.__call__ yield mock # 恢复原函数 dask.base.compute.__call__ = original_compute.__call__ def test_no_compute_invoked(track_compute_calls): import dask import dask.array as da # 测试逻辑:如果这里意外调用了dask.compute或da.compute,断言会失败 track_compute_calls.assert_not_called()
方法2:模块导入前全局补丁(备选)
如果需要确保所有模块导入的都是mock对象,可以在模块加载前打补丁,然后重新加载依赖模块:
from unittest.mock import patch import pytest import importlib import foo @pytest.fixture def mock_my_func(): with patch.object(foo, 'my_func', autospec=True) as mock: # 重新加载bar模块,使其导入被补丁后的函数 import bar importlib.reload(bar) yield mock class TestMyFunc: def test_from_foo(self, mock_my_func): foo.my_func() mock_my_func.assert_not_called() # 失败 def test_from_bar(self, mock_my_func): bar.my_func() mock_my_func.assert_not_called() # 失败
这种方法需要重新加载依赖模块,适合场景较简单的情况,方法1则更通用,无需修改模块加载逻辑。
内容的提问来源于stack exchange,提问作者RuthC
相关产品推荐
相关产品推荐

