Python单元测试模拟类和类方法

11
我觉得这可能相对简单,但是我正在尝试让它正常工作。我想模拟整个类,并指定该类中一个方法的返回值。
我已经查看了这里、几个其他问题以及当然是文档。但仍无法使其工作。请看下面的简单示例。
目录tmp的内容:
tmp
├── __init__.py
├── my_module.py
└── test_my_module.py

my_module.py的内容:

class MyClass:
    def __init__(self):
        # Do expensive operations that will be mocked in testing.
        self.a = 7

    def my_method(self):
        # For sake of simple example, always return 1.
        return 1


def create_class_call_method():
    """Create MyClass instance and call its my_method method, returning
    the result."""
    instance = MyClass()
    value = instance.my_method()
    return value

test_my_module.py 的内容:

import unittest
from unittest.mock import patch, Mock

from tmp import my_module


class MyClassTestCase(unittest.TestCase):

    def test_create_class_call_method(self):
        # Attempt to patch MyClass as well as specify a return_value for
        # the my_method method (spoiler: this doesn't work)
        with patch('tmp.my_module.MyClass',
                   my_method=Mock(return_value=2)):
            value = my_module.create_class_call_method()

        self.assertEqual(value, 2)


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

运行test_my_module.py的结果:

2 != <MagicMock name='MyClass().my_method()' id='140234477124048'>

Expected :<MagicMock name='MyClass().my_method()' id='140234477124048'>
Actual   :2

我尝试过的其他方法:

  • 与其在patch语句中使用..., my_method=Mock(return_value=2)),不如像这样解包一个字典:**{'my_method.return_value': 2}
  • 嵌套with patch语句。外部语句简单,如with patch('tmp.my_module.MyClass'):,内部语句尝试像这样修补my_methodwith patch('tmp.my_module.MyClass.my_method, return_value=2)
  • 使用修补装饰器而不是上下文管理器
  • 将修补语句更改为with patch('tmp.my_module.MyClass') as p:,然后在with语句内尝试设置p,如下所示:p.evaluate = Mock(return_value=2)

希望能得到任何帮助,谢谢。


1
不是你的问题,但你可能想通过@classmethod装饰器创建一个类方法并将其保留在类内部。(尽管它的行为会有一些不同于你实现的方式)。问题表述得很好。 - Error - Syntactical Remorse
1
@Error-SyntacticalRemorse - 感谢您的评论。在我的实际实现中,create_class_call_method稍微复杂一些,实际上是由另一个类使用的,该类创建一个对象并调用其中的一个方法。因此,我认为我不能将其设置为classmethod - blthayer
3个回答

10

我找到了一个更好的解决方案。简而言之,我们需要模拟 MyClassreturn_value。以下是有效的测试代码:

import unittest
from unittest.mock import patch, Mock, MagicMock

from tmp import my_module


class MyClassTestCase(unittest.TestCase):

    def test_create_class_call_method(self):
        # Create a mock to return for MyClass.
        m = MagicMock()
        # Patch my_method's return value.
        m.my_method = Mock(return_value=2)

        # Patch MyClass. Here, we could use autospec=True for more
        # complex classes.
        with patch('tmp.my_module.MyClass', return_value=m) as p:
            value = my_module.create_class_call_method()

        # Method should be called once.
        p.assert_called_once()
        # In the original my_method, we would get a return value of 1.
        # However, if we successfully patched it, we'll get a return
        # value of 2.
        self.assertEqual(value, 2)


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

成功的结果:

Ran 1 test in 0.002s

OK

1
你为什么在类中使用MagicMock,在方法中使用Mock - Mark Moretto

6

我不确定 create_class_call_method 的实现方式,但可以尝试以下方法:

from unittest import mock

class MyClassTestCase(unittest.TestCase):
    @mock.patch('tmp.my_module.MyClass.my_method')
    @mock.patch('tmp.my_module.MyClass.__init__')
    def test_create_class_call_method(self, my_class_init, my_method_mock):
        my_class_init.return_value = None
        my_method.return_value = 2

        value = my_module.create_class_call_method()

        self.assertEqual(value, 2)

感谢您的回答。这个补丁会覆盖整个 MyClass,还是只有 MyClass.my_method(我认为是后者)?对于我的用例,我需要完全模拟 MyClass(我不希望执行其构造函数中的代码),并且我还需要指定调用 MyClass.my_method 的返回值。 - blthayer
@blthayer,它将修补此特定方法。我认为您应该遵循这种方法,因为单元测试的目的是测试一个单元,因此如果您模拟整个类,则可能正在测试多个单元。 - lmiguelvargasf
@blthayer,如果是这样的话,我强烈建议您使用多个模拟对象,并可能将它们声明为方法的注释,而不是使用with语句来避免过多的嵌套。然后使用模拟对象的功能,如return_valueside_effect,在调用实际测试单元之前达到所需的状态。 - lmiguelvargasf
谢谢您的建议。您是否愿意帮我提供一个小例子来说明您所描述的内容?我一开始尝试了类似于您所描述的方法,但是没有成功。让我困惑的是,在测试函数中调用了一个MyClass对象的方法。因此,我不确定如何在那里注入一个模拟对象而不使用patch。 - blthayer
谢谢 - 是的,但正如我所说,该类在测试方法中被实例化。因此,我认为这有点更加微妙。基本上,我想知道如何测试 create_class_call_method 来 a) 断言 MyClass.my_method 已被调用,并且 b) 我们可以指定 MyClass.my_method 的返回值(在我的真实用例中,该返回值将用于其他事情,因此我需要控制它)。 - blthayer
显示剩余3条评论

4

我认为正确的方法在这个答案中被找到了。

注:以下是草图 - 可能没有完全准确地表述 OP 的所有细节。

import unittest
from unittest.mock import patch
from tmp import my_module

class MyClassTestCase(unittest.TestCase):

    @patch('tmp.my_module.MyClass')
    def test_create_class_call_method(self, my_class_mock):
       my_class_mock.return_value.my_method.return_value = 2
       value = my_module.create_class_call_method()
       self.assertEqual(value, 2)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接