python3 模拟不适用于所有路径

python3 mock doesn't work for all paths

生产文件 (production_file.py) 是:

class MyError(Exception):
    pass

class MyClass:
    def __init__(self):
        self.value = None

    def set_value(self, value):
        self.value = value

    def foo(self):
        raise RuntimeError("error!")


class Caller:
    def bar(self, smth):
        obj = MyClass()
        obj.set_value(smth)

        try:
            obj.foo()
        except MyError:
            pass

        obj.set_value("str2")
        obj.foo()

测试文件(test.py):

import unittest

from unittest.mock import patch
from unittest.mock import call
from production_file import MyClass, Caller

class MyTest(unittest.TestCase):
    def test_caller(self):
        with patch('production_file.MyClass', autospec=MyClass) as MyClassMock:
            my_class_mock_obj = MyClassMock.return_value
            my_class_mock_obj.foo.side_effect = [MyError("msg"), "text"]

            caller = Caller()
            caller.bar("str1")

            calls = [call("str1"), call("str2")]

            my_class_mock_obj.set_value.assert_has_calls(calls)

if __name__ == '__main__':
    unittest.main()

以上方法有效。但是,如果我将生产 类 (MyError, MyClass, Caller) 移动到测试文件中,并将补丁更新为:

with patch('test.MyClass', autospec=MyClass) as MyClassMock:

那么实例方法"foo"就不再被mock了。

有人知道这是为什么吗?

我在一些更复杂的代码中也遇到过类似的问题,其中生产代码在 my_package/src/production_file.py 中,而测试代码在 my_package/tests/test_file.py 中。 Python 没有产生路径错误,路径是正确的,但模拟仍然不起作用。

如果你是 运行 test.py__main__ 那么它不是 test.MyClass 而是 __main__.MyClass,或者在这两种情况下 __name__+".MyClass".

通过添加打印语句,我能够确定使用的 class 和修补的 class 是不同的:

class Caller:
    def bar(self, smth):
        print(MyClass) #lets see what we are actually making an instance of...
        obj = MyClass()
        ...

当补丁应用到正在使用的 class 时,您会看到类似这样的内容:

<MagicMock name='MyClass' spec='MyClass' id='4387629656'>

但是当 class 移入 test.py 时,您会看到如下内容:

<class '__main__.MyClass'>

表示:

  1. MyClass 没有应用补丁(至少是用于测试的补丁。)
  2. 需要打补丁的class名称是__main__.MyClass

但是,您的 "more... complicated situation" 很可能因为这样的设置而无法正常工作:

from production_file import MyClass

class MyError(Exception):
    pass


class Caller:
    def bar(self, smth):
        print(MyClass)
        obj = MyClass()
        ...

class MyTest(unittest.TestCase):
    def test_caller(self):
        with patch('production_file.MyClass', autospec=MyClass) as MyClassMock:
            ...

在这种情况下,正在修补 production_file.MyClass 并且正在从 production_file 导入 MyClass,因此正在修补正确的 class 但输出仍然是:

<class 'production_file.MyClass'>

这是因为Class是直接导入到本地命名空间的,所以当补丁应用到production_file本地命名空间仍然不受影响,我们可以检查补丁实际上是适用于:

...
def bar(self, smth):
    print(MyClass)
    from production_file import MyClass as pf_MyClass
    print(pf_MyClass)
...


#output:
<class 'production_file.MyClass'>
<MagicMock name='MyClass' spec='MyClass' id='4387847136'>

如果是这种情况,您只需要导入模块,而不是直接导入 class。然后一旦应用了补丁,您就可以直接从文件中使用它:

import production_file

...
class Caller:
    def bar(self, smth):
        print(production_file.MyClass)
        obj = production_file.MyClass()
        ...

class MyTest(unittest.TestCase):
    def test_caller(self):
        with patch('production_file.MyClass', autospec=MyClass) as MyClassMock:
            ...