【问题标题】:Unit test a method of mocked object单元测试模拟对象的方法
【发布时间】:2014-06-07 15:31:19
【问题描述】:

我试图掌握模拟对象的窍门,但似乎被一些非常基本的东西弄糊涂了。我正在尝试模拟对象 MyClass,然后对其一种方法进行单元测试。这是我的代码:

import mock
import unittest

class MyClass(object):
    def __init__(self, a):
        self.a = a
    def add_two(self):
        return self.a + 2

class TestMyClass(unittest.TestCase):
    @mock.patch('__main__.MyClass')
    def test_add_two(self, dummy_mock):
        m_my_class = mock.Mock()
        m_my_class.a = 10
        result = m_my_class.add_two() # I would expect the result to be 12
        import ipdb;ipdb.set_trace()
        self.assert_equal(result, 12)

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

m_my_class.a = 10 中,我将a 的值设置为to,然后在m_my_class.add_two() 中添加2,我不应该得到12 吗?但是,result 是:

     16         import ipdb;ipdb.set_trace()
---> 17         self.assert_equal(result, 12)
     18 

ipdb> result
<Mock name='mock.add_two()' id='18379792'>

我错过了什么?

既然我通过装饰器将类的位置传递给测试方法@mock.patch('__main__.MyClass'),那么mocked不应该拥有所有的方法吗?因为如果不是,那为什么我们在装饰器中包含什么类很重要?

编辑:

当我运行这段代码时,我仍然得到同样的结果。

class TestMyClass(unittest.TestCase):
    @mock.patch('__main__.MyClass')
    def test_add_two(self, dummy_mock):
        dummy_mock.a = 10
        result = dummy_mock.add_two()
        import ipdb;ipdb.set_trace()
        self.assert_equal(result, 12)

结果:

ipdb> result
<MagicMock name='MyClass.add_two()' id='38647312'>

【问题讨论】:

    标签: python unit-testing mocking


    【解决方案1】:

    修补类几乎肯定不是你想要做的。例如,如果您将测试方法更改为:

    class TestMyClass(unittest.TestCase):
        @mock.patch('__main__.MyClass')
        def test_add_two(self, dummy_mock):
            m_my_class = MyClass(5)
            print m_my_class
    

    你会发现,而不是得到你所期望的:

    <__main__.MyClass object at 0x0000000002C53E48>
    

    你会得到这个:

    <MagicMock name='MyClass()' id='46477888'>
    

    在方法中修补类意味着在方法中任何时候尝试实例化类的东西都会得到一个模拟对象。在您的特定情况下,调用 MyClass(5) 将返回 same 模拟对象,就像调用 dummy_mock 一样。这允许您设置模拟对象,以便在您测试代码时模拟对象将按照您的意愿行事。

    你真的只会将它用于依赖项,而不是你正在测试的类。因此,例如,如果您的类从 SQL 检索数据,您将修补 SQL 类以给您的类提供模拟而不是正常的 SQL 连接。这使您可以单独测试一个类,而不必担心外部因素(如 SQL)会妨碍您。

    不过,在您的示例中,您似乎正在尝试单独测试一种方法。您可以这样做的一种方法是从方法中提取函数并使用它。在代码中:

    def test_add_two(self):
        test_mock = mock.Mock()
        test_mock.add_two = MyClass.add_two.__func__
        test_mock.a = 10
        result = test_mock.add_two(test_mock)
        self.assert_equal(result, 12)
    

    通常您不必为self 参数传递参数,但在这里我们已经提取了您确实需要在self 参数中传递的函数。如果您愿意,可以将函数转换为绑定到模拟对象的方法,如下所示:

    import types
    ...
        test_mock.add_two = types.MethodType(MyClass.add_two.__func__, test_mock, test_mock.__class__)
        result = test_mock.add_two()
    

    如果您正在测试的函数调用任何其他方法,您需要执行此操作或为每个方法模拟它。

    我建议不要过多地使用这种策略,部分原因是需要注意测试方法调用的方法。当然,能够模拟出它所依赖的方法可能正是您想要做的。无论如何,它要求单元测试对方法的工作原理有非常深刻的理解,而不仅仅是它应该产生什么。这使得测试非常脆弱,因此您可能会看到测试中断的频率远高于您正在测试的方法。

    【讨论】:

    • 我不认为我理解第一部分。如果不是这个:result = m_my_class.add_two() 我运行result = m_my_class.no_such_method(),我得到相同的确切结果,所以似乎add_twoMyClass 的方法这一事实与这里无关。
    • @Akavall 对此感到抱歉,我错过了您代码中的导入细节。我已经更新了我的答案。
    • 谢谢,但即使我使用dummy_mock,我仍然得到同样的结果,我更新了问题以显示我正在使用的代码。
    • @Akavall 我比我想象的更生疏。我提醒自己真正发生了什么并再次更新我的答案。
    • @Akavall 我想出了如何单独测试一个方法,这就是你在你的例子中试图做的。我已将其添加到我的答案中。
    【解决方案2】:

    你为什么要嘲笑你的 SUT?这通常是您应该避免做的事情,因为它会带来您不会测试您认为正在测试的内容的风险。

    单元测试的重点是在完全隔离的情况下验证单元的行为。一个单元通常与其他单元协作以提供一些有用的功能。 Test doubles(模拟、伪造等)是用于实现这种隔离的工具。在单元测试中,SUT 的协作者被替换为测试替身,以便minimize the number of moving parts

    您的 SUT MyClass 似乎没有任何合作者。因此,不需要测试替身来单独测试这个单元(它已经是独立的)。这使您可以大大简化您的单元测试:

    import mock
    import unittest
    
    class MyClass(object):
        def __init__(self, a):
            self.a = a
        def add_two(self):
            return self.a + 2
    
    class TestMyClass(unittest.TestCase):
        def test_add_two(self):
            m_my_class = MyClass()
            m_my_class.a = 10
            result = m_my_class.add_two() # I would expect the result to be 12
            import ipdb;ipdb.set_trace()
            self.assert_equal(result, 12)
    
    if __name__  == '__main__':
        unittest.main()
    

    编辑:以下代码演示了如何使用模拟对象。 (免责声明:我通常不使用 Python,所以我的代码可能是 not very idiomatic。但希望核心点仍然有意义。)

    在此示例中,MyClass 添加了一个协作者提供的值,而不是核心值 (2)。

    import mock
    import unittest
    
    class MyClass(object):
        def __init__(self, a, get_value):
            self.a = a
            self.get_value = get_value
        def add_value(self):
            return self.a + self.get_value()
    
    class TestMyClass(unittest.TestCase):
        def test_add_value(self):
            m_test_value = 42
            m_test_a = 10
            m_my_class = MyClass()
            m_get_test_value = mock.Mock(return_value=m_test_value)
            m_my_class.a = test_a
    
            result = m_my_class.add_value()
            import ipdb;ipdb.set_trace()
            self.assert_equal(result, m_test_a + m_test_value)
    
    if __name__  == '__main__':
        unittest.main()
    

    上面的例子使用了一个模拟对象,而不是修补一个类。这是对差异的一个很好的解释:

    Mocking a class: Mock() or patch()?

    【讨论】:

    • 是的,在我的示例中模拟是不必要的(可能不是最好的示例),但我的主要目标是了解模拟。
    • 别担心,我完全明白。我可以更新我的答案以包含使用此 SUT 的模拟对象的演示,这可能会有所帮助(希望如此)。
    • 那太好了:)。
    猜你喜欢
    • 1970-01-01
    • 2014-04-16
    • 1970-01-01
    • 2015-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多