本文最后更新于:2023-07-28T20:25:38+08:00
简介
在单元测试中,Mock是一个很重要的行为,它可以帮助我们排除其他服务的依赖,将需要进行的测试隔离开来。mock
是Python中提供的测试库,在Python2中,mock
是独立的库,而在Python3中,mock
被加入了标准单元测试库unittest
当中,官方文档为unittest.mock|docs 。
在mock
库中,有两个非常重要的概念,分别是Mock
以及patch
。Mock
指的是mock
库中提供的Mock
类,它产生的对象可以模拟各种行为,简单来说就是根据程序员指定输入和输出进行工作。而patch
则用于在特定作用范围内执行模拟,将指定的实际对象变为Mock
对象,此时实际对象就会按照我们预先指定的方式进行工作。Mock
和patch
结合工作,联系紧密。
Mock
quick start
Mock是unittest.mock库中提供的一个类,要使用它首先得进行对象的创建。
Mock类的构造方法签名如下:
1 class unittest .mock.Mock(spec=None , side_effect=None , return_value=DEFAULT, wraps=None , name=None , spec_set=None , unsafe=False , **kwargs)
其中最重要的两个参数是return_value
和side_effect
。
return_value
用于指定该Mock对象被调用时的返回值,且该返回值始终不变。
side_effect
用于指定可变的返回值或者抛出特定的异常,该参数可以设置为三种类型:
如果设置为异常类,则执行该Mock对象时会抛出异常
如果设置为方法,则执行该Mock对象时,会将传入的参数传递给设置的方法,然后返回方法的返回值
如果设置为可迭代对象,则执行该Mock对象时会按照迭代顺序进行返回,得到可变的返回值
需要注意的是side_effect
的优先级高于return_value
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from unittest.mock import Mock mock = Mock(return_value=1 )assert mock() == 1 mock = Mock(side_effect=RuntimeError)def add_one (num: int ): return num + 1 mock = Mock(side_effect=add_one)assert mock(2 ) == 3 mock = Mock(side_effect=[1 , 2 , 3 ])assert mock() == 1 assert mock() == 2 assert mock() == 3
我们可以手动指定某个对象的方法,将其使用Mock进行替换:
1 2 3 4 5 6 7 8 class TestClass : def func (self, a, b ): return a + b tc = TestClass() tc.func = Mock(return_value=666 )print (tc.func(1 , 2 ))
Mock类同样支持对Python中所有的magic方法进行mock。不过如果需要使用到magic方法,最简单的方法是使用MagicMock
类。MagicMock
类是Mock
类的一个子类,它实现了所有常用的magic方法。
1 2 3 4 5 6 7 8 9 10 11 from unittest.mock import Mock, MagicMock mock = Mock() mock.__str__ = Mock(return_value="my mock" )print (mock) magic_mock = MagicMock() magic_mock.__str__.return_value = "my magic mock" print (magic_mock)
另外,我们还可以使用create_autospec
方法来创建与原对象相同的api。
创建一个具有相同入参签名的Mock方法:
1 2 3 4 5 6 7 8 9 10 11 from unittest.mock import create_autospecdef func (a: int , b: int , c: int ): return a + b + c mock_func = create_autospec(func, return_value="abc" )print (func(1 , 2 , 3 )) print (mock_func(1 , 2 , 3 )) print (mock_func(1 , 2 ))
创建一个具有相同属性以及方法签名的Mock测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from unittest.mock import create_autospecclass TestCase : test_attr = 1 def test_func (self, num: int ): return self.test_attr + num mock_class = create_autospec(TestCase, return_value=1 )print (mock_class.test_attr) print (mock_class.test_attr_not_exist) print (mock_class.test_func) mock_class.test_func.return_value = 123 print (mock_class.test_func(1 )) print (mock_class.test_func_not_exist)
这里我们得到的Mock测试类具有与TestCase相同的属性和方法签名,不过对应的属性和方法签名仍然是一个Mock类,我们可以像之前的操作一样指定它的return_value等相关信息。
相关方法
Mock类还有其他一些常用的方法,主要是一些assert方法。因为Mock对象除了可以用来模拟对象、属性和方法等,它还会记录自身被使用的过程。利用相关assert方法可以验证代码是否被执行过,被怎样执行过。
assert_called(*args, **kwargs)
:断言mock至少被调用一次
assert_called_once(*args, **kwargs)
:
断言mock调用仅一次
assert_called_with(*args, **kwargs)
:断言mock以某种参数至少调用一次,且符合条件的调用是最近一次
assert_called_once_with(*args, **kwargs)
:
断言mock调用仅一次,并且调用参数符合条件
assert_any_called(*args, **kwargs)
:断言mock以某种参数曾经被调用过,注意上面的assert_called_with()
必须是最近的那次调用符合断言,而any不需要
assert_has_calls(calls, any_order=False)
:断言mock被按照的特定一组调用的方式调用过。如果any_order
是False,那么必须满足calls中的调用顺序,而且必须是连续的,如果any_order
是True,那么就只需要执行了calls中的调用即可
1 2 3 4 5 6 7 8 from unittest.mock import Mock, call mock = Mock(return_value="123" ) mock(1 , 2 ) mock(2 , 3 ) mock.assert_has_calls([call(2 , 3 ), call(1 , 2 )], any_order=True ) mock.assert_has_calls([call(2 , 3 ), call(1 , 2 )], any_order=False )
assert_not_called()
: 断言没有被调用
reset_mock(*args, return_value=False, side_effect=False)
:重置mock对象的所有调用
call_count
: 返回调用次数
call_args
:返回最后调用的参数
call_args_list
:返回调用的历史参数列表
method_calls
:返回调用的历史方法列表
其他Mock类
NonCallableMock
:一个不可被调用的Mock类,return_value
和side_effect
这两个参数对它来说是无意义的
PropertyMock
:一个专门用于替换属性的Mock类,它提供了属性对应的get和set方法
AsyncMock
:MagicMock的异步版本。AsyncMock对象会像一个异步数一样运行,它的调用的返回值是一个awaitable对象,这个awaitable对象返回
side_effect
或者 return_value
指定的值
Patch
quick start
patch
可以帮助我们在特定范围内执行模拟,在代码运行时将指定的对象变为Mock对象。patch
有多种使用方式,可以使用装饰器、with语句以及利用start()
和stop()
方法指定模拟的开始和结束。
patch
实际上是一个方法,方法签名如下:
1 unittest.mock.patch(target, new=DEFAULT, spec=None , create=False , spec_set=None , autospec=None , new_callable=None , **kwargs)
其中参数target
用于指定被模拟的对象,是一个类似package.module.className
格式的字符串;参数new_callable
可以用于指定最终创建的模拟对象类型,默认情况下是MagicMock类型的Mock对象。
patch装饰器的使用方式如下。通过target参数指定被模拟的类,创建出的Mock对象通过参数传递给方法。此时的模拟范围是函数的范围。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from unittest.mock import patchclass SomeClass : pass @patch('__main__.SomeClass' ) def func (a, b, mock_class ): print (a) print (b) print (mock_class)if __name__ == '__main__' : func(2 , 3 ) ''' print content: 2 3 <MagicMock name='SomeClass' id='4488196304'> '''
patch
with语句的使用方式如下。通过上下文管理的方式设置模拟范围,创建出的Mock对象通过as语句进行传递。
1 2 3 4 with patch('__main__.SomeClass' ) as mock_class: print (mock_class)
patch手动指定作用范围的使用方式如下。
1 2 3 4 5 6 patcher = patch('__main__.SomeClass' ) mock_class = patcher.start()print (mock_class) patcher.stop()
在上面的情况中,使用注解的情况非常常见,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from unittest.mock import patchclass SomeClass : num = 1 def func (self, other_num ): return self.num + other_num@patch('__main__.SomeClass.func' , return_value="mock" ) def test_patch_some_class (mock_func ): some_class = SomeClass() print (some_class.func(2 ))if __name__ == '__main__' : test_patch_some_class()''' print content: mock '''
其他方法
patch.object()
:用于模拟对象的属性,使用Mock对象模拟对象的属性。初始化时使用参数target
指定对象,使用参数attribute
设置模拟的属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 def test_patch_some_class (): with patch.object (SomeClass, 'func' , return_value="mock" ) as mock_func: some_class = SomeClass() print (some_class.func(2 ))if __name__ == '__main__' : test_patch_some_class() ''' print content: mock '''
patch.dict()
:用于模拟dict类型的对象,在模拟结束时恢复被模拟对象的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def test_patch_dict (): foo = {'key' : 'value' } original = foo.copy() with patch.dict (foo, {'new_key' : 'new_value' }, clear=True ): print (foo) assert foo == {'new_key' : 'new_value' } print (foo) assert foo == originalif __name__ == '__main__' : test_patch_dict()''' print content: {'new_key': 'new_value'} {'key': 'value'} '''
patch.multiple()
: 用于同时模拟多个对象
1 2 3 4 5 6 7 8 9 10 thing = object () other = object ()@patch.multiple('__main__' , thing=DEFAULT, other=DEFAULT ) def test_function (thing, other ): assert isinstance (thing, MagicMock) assert isinstance (other, MagicMock) test_function()
如果在with语句中使用,则返回一个字典对象:
1 2 3 4 5 6 7 8 thing = object () other = object ()with patch.multiple('__main__' , thing=DEFAULT, other=DEFAULT) as values: assert 'other' in repr (values['other' ]) assert 'thing' in repr (values['thing' ]) assert values['thing' ] is thing assert values['other' ] is other
特殊情况
Method not exist
在一些时候,我们可能需要模拟一个不存在的方法,例如下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 @patch('__main__.SomeClass.func_not_exist' , return_value="mock" ) def test_patch_some_class (mock_func ): some_class = SomeClass() print (some_class.func(2 ))if __name__ == '__main__' : test_patch_some_class() ''' AttributeError: <class '__main__.SomeClass'> does not have the attribute 'func_not_exist' '''
这里我们模拟了一个不存在的方法,默认情况是会报错的,报无法找到属性的错。此时我们可以指定create=True
,此时如果patch对象不存在的话,会自动创建,不会报错。
1 2 3 4 5 6 7 8 9 10 11 12 @patch('__main__.SomeClass.func_not_exist' , return_value="mock" , create=True ) def test_patch_some_class (mock_func ): some_class = SomeClass() print (some_class.func(2 ))if __name__ == '__main__' : test_patch_some_class()''' print content: 3 '''
上面的代码不会报错,并且由于我们并没有Mock真实的逻辑,因此输出也是正常的3。
Module not exist
在一些时候,我们可能要模拟一个方法,但是这个方法所属的Module又没有安装,例如下面的代码,正常运行的话,会报错为Module not found
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from unittest.mock import patchdef method_need_to_be_test (): try : from xxx.yyy.zzz import some_method except ModuleNotFoundError: raise RuntimeError("Module not found, please make sure the dependency have installed" ) return some_method()def test_method (): method_need_to_be_test()if __name__ == '__main__' : test_method()
为了解决Module不存在的情况,我们首先需要安装surrogate
。这个库允许为一个不存在的Module创建测试桩。
安装了依赖之后,利用@surrogate
装饰器为不存在的Module创建测试桩之后,再利用@patch
装饰器为这个不存在的方法进行模拟和Mock,如下所示。之后再次运行,就可以得到Mock后的结果了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from unittest.mock import patchfrom surrogate import surrogatedef method_need_to_be_test (): try : from xxx.yyy.zzz import some_method except ModuleNotFoundError: raise RuntimeError("Module not found, please make sure the dependency have installed" ) return some_method()@surrogate('xxx.yyy.zzz.some_method' ) @patch('xxx.yyy.zzz.some_method' , return_value="mock result" ) def test_method (mock_func ): print (method_need_to_be_test())if __name__ == '__main__' : test_method() ''' print content: mock result '''
参考文章
单元测试mock模块介绍
Python内置库
unittest.mock 的基础使用
How
do I mock the hierarchy of non-existing
modules|StackOverflow