模拟超类 __init__ 方法或超类作为一个整体进行测试

Mock superclass __init__ method or superclass as a whole for testing

我想测试一个我写的Pythonclass,是这样的:

from external_library import GenericClass

class SpecificClass(GenericClass):

  def __init__(self, a, b, c):
    super(SpecificClass, self).__init__(a, b)
    self.c = c

  def specific_method(self, d):
    self.initialize()
    return d + self.c + self.b + self.a.fetch()

GenericClass 由外部库定义 (external_library):

class GenericClass(object):

  def __init__(self, a, b):
    self.a = a
    self.b = b + "append"

  def initialize(self):
    # it might be an expensive operation
    self.a.initialize()

  def specific_method(self, d):
    raise NotImplementedError

我应该如何测试specific_method?除了 GenericClass.initialize,我应该将 GenericClass 作为一个整体进行模拟,GenericClass.__init__super 还是 GenericClass.aGenericClass.b?还是我处理问题的方法完全错误?

请使用 mock 作为模拟库,使用 pytest 作为测试框架。该解决方案需要 Python2.6+ 兼容。

提前致谢。

从综合示例中猜出最佳方法有点困难。什么时候可以只使用所需的模拟,并且尽可能多地使用真实对象是最好的。但这是一个权衡游戏,当创建真实对象很困难并且依赖性很深时,扭曲的模拟可能是非常强大的工具。此外,模拟为您提供了很多测试可以发挥作用的感觉点。这是要付出代价的:有时通过使用模拟,您可以隐藏仅通过集成测试即可引发的错误。

恕我直言,对于您的情况,最好只是模拟 a 并设置 fetch 的 return 值。您还可以测试 a.initialize() 调用

如果你的目标只是模拟 a.initialize()(当然 a.fetch() 我想没有 initialize() 就没有意义);最好的就是 patch a object.

无论如何,最后的测试是关于给超级class打补丁的。在您的情况下,这有点棘手,您必须同时修补 __init__ 并注入 ab 属性。如果你担心 GenericClass 在 init 中做了什么你必须直接修补它 __init__ 方法:修补 class 是没有用的,因为它的引用已经在 SpecificClass 定义中(它是由 super 而不是 GenericClass.__init__(...) 解决)。也许在真实的代码中你应该反对 ab 的只读 属性:同样,如果你可以使用真实的对象并且尽可能少地修补 method/object 是最好的选择。我喜欢 mock 框架,但有时使用它并不是最好的选择。

还有一件事:我使用 patch.multiple 只是为了得到一个更紧凑的示例,但是通过两个 patch.object 完成相同的工作来修补该方法。

import unittest
from mock import Mock, patch, DEFAULT, PropertyMock
from specific import SpecificClass


class A():
    def initialize(self):
        raise Exception("Too slow for tests!!!!")

    def fetch(self):
        return "INVALID"


class MyTestCase(unittest.TestCase):
    def test_specific_method(self):
        a = A()
        with patch.multiple(a, initialize=DEFAULT, fetch=DEFAULT) as mocks:
            mock_initialize, mock_fetch = mocks["initialize"], mocks["fetch"]
            mock_fetch.return_value = "a"
            sc = SpecificClass(a, "b", "c")
            self.assertEqual("dcbappenda", sc.specific_method("d"))
            mock_initialize.assert_called_with()

    def test_sanity_check(self):
        a = A()
        sc = SpecificClass(a, "b", "c")
        self.assertRaises(Exception, sc.specific_method, "d")

    #You can patch it in your module if it is imported by from .. as ..
    @patch("specific.GenericClass.__init__", autospec=True, return_value=None)
    def test_super_class(self, mock_init):
        sc = SpecificClass("a", "b", "c")
        mock_init.assert_called_with(sc, "a", "b")
        #Inject a and b
        sc.a = Mock()
        sc.b = "b"
        #configure a
        sc.a.fetch.return_value = "a"
        self.assertEqual("dcba", sc.specific_method("d"))
        sc.a.initialize.assert_called_with()