对模拟对象的方法进行单元测试
我正在尝试理解如何模拟对象,但对一些基本的东西感到困惑。我想模拟一个叫做 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 个回答
你为什么要对你的被测试单元(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()
上面的例子使用了模拟对象,而不是对一个类进行补丁。这里有一个很好的解释,说明了两者的区别:
在这里,修改类几乎肯定不是你想要的做法。例如,如果你把测试方法改成这样:
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()
如果你正在测试的函数调用了其他方法,你需要为每个方法要么这样做,要么进行模拟。
我建议不要过多使用这种策略,部分原因是需要处理被测试方法调用的其他方法。当然,能够模拟它所依赖的方法可能正是你想要的。无论如何,这要求单元测试对方法的工作原理有很深的理解,而不仅仅是知道它应该产生什么结果。这会让测试变得非常脆弱,导致测试失败的频率远高于你正在测试的方法。