文章地址 https://hustyichi.github.io/2018/11/24/mock/
官方文档 https://docs.python.org/zh-cn/3/library/unittest.mock.html
mock
mock是python中的测试库,主要用于提供测试桩。简单解释一下,在我们进行单元测试时,如果依赖其他服务,那么如何隔离进行测试呢。此时会建立一个测试桩,模拟所依赖的服务,从而避免依赖服务的干扰,专注测试所需的模块。而mock库就可以帮助我们很方便地建立测试桩,在python2中,mock是独立的库。在python3中,mock就被加入unittest单元测试库中了,足见mock库在单元测试中的不可或缺性。需要进行测试的同志们,赶紧学习起来吧。
本文的大部分内容都参考自官方文档,想深入研究的可以移步官方文档。
基础概念
mock库中有两个基础的概念,
- 一个是Mock类,Mock是一个灵活而且功能强大的类,用于模拟依赖的模块
- 一个是patch,patch用于在特定作用域内执行模拟。
Mock类是使用十分简单,直接创建Mock对象,简单情况下只需要通过return_value
指定返回值,代码如下所示:
m = Mock(return_value=3)
assert m() == 3
上面的代码中,创建了Mock对象,这个对象会在执行之后始终返回3,在测试中如果有模块需要返回特定值,就可以使用Mock类进行替换
目前我们已经有了Mock类作为模拟对象,接着就可以展示使用patch指定被模拟对象,patch的使用如下所示:
with patch(product_func_path) as mock_method:
mock_method.return_value = 3
assert product_func() == 3
在上面的代码中,将需要模拟的方法product_func()
的路径product_func_path
作为参数传递给patch方法,并创建Mock对象mock_method
进行模拟。可以看到指定mock_method
的返回值为3,然后执行product_func()
方法,最终返回值为3。可以看到成功使用Mock类模拟了特定的方法,在with作用域内,调用被模拟方法product_func()
时,实际调用的都是Mock类的方法。
Mock 类
在上面的介绍我们已经知道,Mock类是用于模拟项目的任意模块,可以用于模拟方法,也可以模拟类对象。如果Mock类的灵活性得不到保证,那么整个mock库的可用性就会大打折扣。事实上,Mock类是一个相当灵活的类,可以对相当复杂的类或方法进行模拟。
首先来看Mock类的初始化方法:
class unittest.mock.Mock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, unsafe=False, **kwargs)
创建参数中最重要的两个参数是side_effect
和 return_value
,下面依次进行介绍:
return_value
用于表示此Mock对象的不变的返回值。此返回值在第一次调用Mock对象时生成,后面多次调用对象,都返回此相同的返回值side_effect
用于指定可变的返回值或抛出特定的异常。此参数可以设置为下面三种类型:- 异常类, 如果
side_effect
设置为异常类,那么执行Mock对象时就会抛出此异常 - 方法,如果
side_effect
设置为特定的方法,那么执行Mock对象时,就会将Mock对象执行的参数传递给side_effect
指定的方法,并将此方法返回的值作为Mock对象的返回值 - 迭代器,如果
side_effect
设置为可迭代的对象,那么执行Mock对象就会从这个可迭代的对象中获取一个值进行返回
- 异常类, 如果
下面可以展示side_effect
参数的使用:
m = Mock(side_effect=Exception('boom'))
with pytest.raises(Exception):
m()
可以看到上面的Mock类执行时,会抛出Exception,适用于测试会抛出特定异常的场景
m = Mock(side_effect=[3, 4, 5])
assert m() == 3
assert m() == 4
assert m() == 5
可以看到上面的代码中,多次调用Mock会返回不同的值,每次的值是从可迭代对象[3, 4, 5]
中返回一个值。适用于方法执行后会返回了一系列特定的值的情况
还有一种情况,可以设置Mock为特定的方法,每次调用Mock对象就是执行此方法。大家可以后续自己尝试,同样可以获取到不同的返回值。
Mock方法与属性
Mock类提供了一些方法与属性,方便进行测试
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()
与assert_called_once_with()
必须是最近的那次调用符合断言assert_has_calls(calls, any_order=False)
断言mock被按照的特定一组调用的方式调用过。如果any_order
是False,那么必须满足calls中的调用,而且必须是连续的,如果any_order
是True,那么就只需要执行了calls中的调用就可以了assert_not_called()
断言没有被调用reset_mock(*args, return_value=False, side_effect=False)
重置mock对象的所有调用attach_mock()
将mock附加在mock对象的属性上called
mock是否被调用的值call_count
调用次数return_value
设置mock的返回值side_effect
可以设置为特定的方法,迭代器或者一个异常。设置为None可以取消side_effect
的影响call_args
最后调用的参数__class__
指定mock的类型,支持isinstance()
判断
其他Mock类
MagicMock
是Mock类的子类,与Mock相比,默认实现了大部分的魔术方法NonCallableMock
是不可调用的Mock类,适合模拟模拟不可调用的类对象NonCallableMagicMock
是不可调用的MagicMock类
patch
patch用于在特定范围内执行模拟。patch有多种使用方法,可以
- 使用装饰器方法,
- 使用with语句,
- 可以通过
start()
和stop()
方法指定模拟的开始和结束。笔者注: 不同的使用方法,作用范围不同
首先查看patch的初始化方法:
unittest.mock.patch(target, new=DEFAULT, spec=None, create=False, spec_set=None, autospec=None, new_callable=None, **kwargs)
patch方法的初始化参数中,最重要的就是target
,target
用于指定被模拟的对象,是一个类似package.module.className
格式的字符串,其次参数new_callable
可以用于指定最终创建的模拟对象类型,默认情况下是MagicMock类型的Mock对象
patch 使用方法
patch可以采用多种方式使用,可以采用
- 装饰器方法,也可以
- 使用with语句,
- 也可以手工指定,
下面依次进行介绍:
首先是装饰器的方式,通过patch装饰器,通过target指定参数指定被模拟的对象,而创建的模拟对象会通过参数传递给方法,代码类似如下所示:
@patch('__main__.someClass')
def function(normal_arguments, mock_class)
assert mock_class is someClass
可以看到上面的代码模拟了someClass
类,创建的模拟对象通过参数mock_class
传递给方法,在方法中可以看到mock_class
与 someClass
类型一致。通过此方式指定模拟的范围是函数内部,当函数执行结束,模拟就不再生效
其次是with语句的使用方式,即通过上下文管理的方式设置模拟范围。使用此方式创建的Mock对象通过as
语句传递给后面使用,代码如下所示:
with patch('__main__.someClass') as mock_class:
ret = mock_class.return_value
assert mock_class is someClass
assert someClass() is ret
可以看到上面的代码,模拟了someClass
类,模拟对象为mock_class
,在方法中,可以看到mock_class
与someClass
类型一致,而被模拟对象someClass
执行后的结果与mock_class
的返回值,即通过return_value
指定的值一致
最后还可以手动指定模拟的范围,实现的代码如下所示:
patcher = patch('__main__.someClass')
mock_class = patcher.start()
assert someClass is mock_class
patcher.stop()
可以看到一样使用mock_class
模拟了someClass
类,最终通过patch.start()
和 patch.stop()
指定模拟的开始和结束
其他patch方法
patch.object()
用于模拟对象的属性,使用Mock对象模拟对象的属性。初始化时使用参数target
指定对象,使用参数attribute
设置模拟的属性。patch.dict()
用于模拟dict类型的对象,在模拟结束时恢复被模拟对象的数据patch.multiple()
用于同时模拟多个对象
总结
mock库的使用只需要掌握两个基础概念,使用Mock类创建模拟对象,模拟任意方法或类,使用patch()
方法执行模拟,掌握了这个概念,理解mock库就轻而易举了。