在本博客系列的第一部分,我们开始研究如何使用嘲讽来改进我们的单元测试。在这篇文章中,我们将对此进行扩展。
嘲弄一个属性
另一个常见的情况是模拟一个对象或类的属性。 我们可以使用mock.patch.object来实现这一点。
假设我们有一个函数,它将尝试从一个被传递的文件句柄中读取数据,我们想测试一下如果读取失败它将做什么。
def read_from_handle(fh):
try:
return len(fh.read())
except IOError:
return None
1 from django.test import TestCase
2 from unittest import mock
3 from ... import read_from_handle
4
5 class TestClass(TestCase):
6 def test_read_error(self):
7 fh = open("testfile", "r")
8 with mock.patch.object(fh, "read") as mock_read:
9 mock_read.side_effect = IOError("fake error for test")
10 retval = read_from_handle(fh)
11
12 self.assertIsNone(retval)
在第8行,mock.patch.object(fh, "read")意味着,当mock生效时,我们将用我们的mock对象替换对象fh的read属性的值。同样,默认情况下,mock.patch.object只是创建一个新的mock对象来使用。
在第9行,通过给模拟对象的side_effect属性分配一个异常,我们表明当调用时,不是返回一个值,而是应该引发异常。 我们的函数应该捕获异常并返回None,所以我们用通常的方法检查其返回值是否为None。
我尽量使用mock.patch.object而不是mock.patch,因为当我试图找出应用mock的地方时,它对我更有意义。但这两种方法都有其用途。
在一个类上模拟一些东西
如果我们不能访问一个对象来模拟它,也许是因为它还不存在,我们可以把模拟应用到将被用来创建对象的类上。 我们只需要确保模拟在对象被创建时是有效的。
class SomeClass:
def some_function(self):
return 1
def function_to_test():
obj = SomeClass()
return obj.some_function()
from django.test import TestCase
from unittest import mock
from ... import function_to_test, SomeClass
class TestClass(TestCase):
def test_object_all(self):
with mock.object.patch(SomeClass, "some_function") as mock_function:
mock_function.return_value = 3
self.assertEqual(3, function_to_test())
通过在类对象上模拟some_function,我们安排由它创建的实例也会有some_function是我们的模拟对象。
mock.patch和mock.patch.object如何工作
我有一个关于mock.patch如何工作的心理模型,对我来说很有用。我确信在现实中它要复杂得多,但我想象它的工作是这样的。
with mock.patch('pkg.module.name') as xyz:
run code
# which is implemented something like
import sys.modules
module = sys.modules["pkg"]["module"]
saved_value = module["name"]
mock_object = mock.MagicMock()
module["name"] = mock_object
xyz = mock_object
[ run code ]
module["name"] = saved_value
它在适当的模块中按名称找到引用,保存其值,将其改为模拟对象,完成后,恢复其值。
而我想象中的mock.patch.object也是如此。
with mock.patch.object(some_object, "attrname") as xyz:
run code
# which is implemented something like
saved_value = getattr(some_object, "attrname")
mock_object = mock.MagicMock()
setattr(some_object, "attrname", mock_object)
xyz = mock_object
[ run code ]
setattr(some_object, "attrname", saved_value)
这就更简单了。
控制模拟对象的行为
我们已经看到,我们可以在一个模拟对象上赋值给.return_value,每当模拟对象被调用时,我们设置的值将被返回。
对于更复杂的行为,我们可以赋值给.side_effect。
- 指定一个值的列表,每当模拟对象被调用时,列表中的下一个值将被返回。
- 指定一个异常实例或类,调用模拟对象将引发该异常。
- 指定一个可调用对象,任何时候调用模拟对象,参数将被传递给可调用对象,而可调用对象的返回值将从模拟对象返回。
大多数在构造模拟对象时可以传递的参数也可以在以后作为属性分配,所以阅读模拟类的文档应该会给你更多的想法。
确定对一个模拟对象所做的事情
你可以用mock_object.assert_called()断言一个模拟对象已经被调用。通过使用mock_object.assert_called_with(*args,**kwargs)来断言它的最后一次调用有你所期望的参数,这更有用。或者使用mock_object.assert_not_called()来断言该mock没有被调用。
如果你不关心某些参数,你可以把mock.ANY传递给assert_called...方法,以代替这些参数,无论该参数是什么值,断言都会通过。
mock_object.assert_called_with(1, 2, mock.ANY, x=3, y=mock.ANY)
关于同一主题的更多变化,请参见mock类文档。
如果你想检查一些没有内置方法的东西,你可以访问mock_object.call_args_list,它是一个(args, kwargs)对的列表,每次调用模拟对象时都有一个。
注意:当在模拟对象上拼写方法名时要小心,因为如果你拼错了一个,而不是一个错误,模拟对象就会用这个名字定义一个模拟方法,并返回另一个模拟对象。 有时你可以通过在创建模拟对象时使用spec参数来避免这种情况,但这已经超出了本系列博客的范围。