单元测试mock模块介绍

mock

Posted by Bryan on November 24, 2018

mock

mock是python中的测试库,主要用于提供测试桩。简单解释一下,在我们进行单元测试时,如果依赖其他服务,那么如何隔离进行测试呢。此时会建立一个测试桩,模拟所依赖的服务,从而避免依赖服务的干扰,专注测试所需的模块。而mock库就可以帮助我们很方便地建立测试桩,在python2中,mock是独立的库。在python3中,mock就被加入unittest单元测试库中了,足见mock库在单元测试中的不可或缺性。需要进行测试的同志们,赶紧学习起来吧。

本文的大部分内容都参考自官方文档,想深入研究的可以移步官方文档

基础概念

mock库中有两个基础的概念,一个是Mock类,一个是patch,Mock是一个灵活而且功能强大的类,用于模拟依赖的模块。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_effectreturn_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方法的初始化参数中,最重要的就是targettarget 用于指定被模拟的对象,是一个类似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_classsomeClass 类型一致。通过此方式指定模拟的范围是函数内部,当函数执行结束,模拟就不再生效

其次是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_classsomeClass 类型一致,而被模拟对象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库就轻而易举了。