对模拟对象的方法进行单元测试

1 投票
2 回答
1476 浏览
提问于 2025-04-18 03:51

我正在尝试理解如何模拟对象,但对一些基本的东西感到困惑。我想模拟一个叫做 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 的值设置为 10,然后在 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'),所以模拟的对象应该有所有的方法吧?如果没有,那我们在装饰器中包含哪个类又有什么关系呢?

编辑:

当我运行这段代码时,结果还是一样。

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'>

2 个回答

2

你为什么要对你的被测试单元(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,所以我的代码可能不太符合习惯用法。不过希望核心观点还是能让你明白。)

在这个例子中,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()

上面的例子使用了模拟对象,而不是对一个类进行补丁。这里有一个很好的解释,说明了两者的区别:

模拟一个类:使用 Mock() 还是 patch()?

2

在这里,修改类几乎肯定不是你想要的做法。例如,如果你把测试方法改成这样:

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) 会返回和调用 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()

如果你正在测试的函数调用了其他方法,你需要为每个方法要么这样做,要么进行模拟。

我建议不要过多使用这种策略,部分原因是需要处理被测试方法调用的其他方法。当然,能够模拟它所依赖的方法可能正是你想要的。无论如何,这要求单元测试对方法的工作原理有很深的理解,而不仅仅是知道它应该产生什么结果。这会让测试变得非常脆弱,导致测试失败的频率远高于你正在测试的方法。

撰写回答