You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何测试不同命名空间导入的函数未被调用?

统一拦截同一函数的不同导入调用问题

示例代码结构

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 01:27:49