pytest中使用mock
2018-02-13 23:54:09 +08 字数:2030 标签: Python Test为什么要用mock? ¶
单元测试的条件有限,在测试过程中,有时会遇到难以准备的环境。 比如,与服务器的网络交互、对数据库的读写等。
传统思路,是利用fixture进行测试环境准备。 这种做法的优点是,与真实环境非常相似,测试效果好。 但缺点是,测试代码开发时间长,测试执行时间也很长。
另一种思路是,准备一个虚假的沙箱,对代码的执行效果进行模拟。 这样虽然不能测试真正的最终效果,但是却更容易保证100%测试覆盖率,并且避免重复测试,降低测试执行时间。
mock是什么? ¶
mock的意思是虚假的、模拟的。 在Python的单元测试中,由于一切都是对象(object)。 而mock的技术,就是在测试时,不修改源码的前提下,替换某些对象,模拟测试环境。
比方说,一个函数里,调用了三个函数。 只需要测试这三个函数是否被依次调用即可,而无需测试真实的调用修改。
def put_elephent_into_fridge(elepent, fridge):
fridge.open()
fridge.put(elepent)
fridge.close()
假设fridge
这个类已经完全被测试覆盖了。
这里如果用传统的测试方法,只能让这三个方法再被测试一遍。
而如果把fridge
换成一个mock,那么就可以避免重复测试,并且达到测试目的。
在Python标准库中,有unittest这个库。 在Python 3.3以后,其中包含一个unittest.mock,就是Python最常用的mock库。 此外,PyPI上还有一个mock库,是进入标准库前的mock,可以在旧的版本使用。
虽然可以直接在pytest的测试中,直接使用mock,但是并不方便。 所以,在此直接推荐pytest-mock。
pytest-mock ¶
pytest-mock是一个pytest的插件,安装即可使用。
它提供了一个名为mocker
的fixture,仅在当前测试function或method生效,而不用自行包装。
object ¶
mock一个object,是最常见的需求。 由于function也是一个object,以下以function举例。
import os
def rm(filename):
os.remove(filename)
def test_rm(mocker):
filename = 'test.file'
mocker.patch('os.remove')
rm(filename)
os.remove.assert_called_once_with(filename)
这里在给os.remove
打了一个patch,让它变成了一个MagicMock。
然后利用assert_called_once_with
,查看它是否被调用一次,并且参数为filename
。
注意:只能对已经存在的东西使用mock。
method ¶
有时,仅仅需要mock一个object里的method,而无需mock整个object。
例如,在对当前object的某个method进行测试时。
这时,可以用patch.object
。
class ForTest:
field = 'origin'
def method():
pass
def test_for_test(mocker):
test = ForTest()
mock_method = mocker.patch.object(test, 'method')
test.method()
assert mock_method.called
assert 'origin' == test.field
mocker.patch.object(test, 'field', 'mocked')
assert 'mocked' == test.field
上例中,分别对field和method进行了mock。 当然,对一个给定module的function,也能使用。
def test_patch_object_listdir(mocker):
mock_listdir = mocker.patch.object(os, 'listdir')
os.listdir()
assert mock_listdir.called
用spy包装 ¶
如果只是想用MagicMock包装一个东西,而又不想改变其功能,可以用spy
。
def test_spy_listdir(mocker):
mock_listdir = mocker.spy(os, 'listdir')
os.listdir()
assert mock_listdir.called
与上例中的patch.object
不同的是,上例的os.listdir()
不会真的执行,而本例中则会真的执行。
MagicMock ¶
即使使用pytest-mock简化使用过程,对mock本身还是要有基本的了解,尤其是MagicMock。
MagicMock是属于unittest.mock中的一个类,是Mock这个类的一个默认实现。
在构造时,还常用return_value
、side_effect
和wraps
这三个参数。
(当然,还有其它不常用参数,详见Mock。)
import os
import pytest
def name_length(filename):
if not os.path.isfile(filename):
raise ValueError('{} is not a file!'.format(filename))
print(filename)
return len(filename)
def test_name_length0(mocker):
isfile = mocker.patch('os.path.isfile', return_value=True)
assert 4 == name_length('test')
isfile.assert_called_once()
isfile.return_value = False
with pytest.raises(ValueError):
name_length('test')
assert 2 == isfile.call_count
def test_name_length1(mocker):
mocker.patch('os.path.isfile', side_effect=TypeError)
with pytest.raises(TypeError):
name_length('test')
def test_name_length2(mocker):
mocker.patch('os.path.isfile', return_value=True)
mock_print = mocker.patch('builtins.print', wraps=print)
mock_len = mocker.patch(__name__ + '.len', wraps=len)
assert 4 == name_length('test')
assert mock_print.called
assert mock_len.called
以上展示了return_value
、side_effect
和wraps
的用法。
不仅可以在构造MagicMock时作为参数传入,可以在那之后调整。
return_value
修改了os.path.isfile
的返回值,控制程序执行流,而无需在文件系统中生成文件。
side_effect
可以令某些函数抛出指定的异常。
wraps
可以既把某些函数包装成MagicMock,又不改变它的执行效果(这一点类似spy
);当然,也完全可以替换成另一个函数。
在Python 3中,内置函数可以通过builtins.*
来进行mock。
然而某些内置函数牵涉甚广,比如len
,不适合在Builtin作用域进行mock,可以在被测试的函数所在的Global作用域进行mock。
如本例中,就对当前module的Global作用域里的len
进行了mock。
此外,上例中还展示了MagicMock中的一些属性,如assert_called_once
、call_count
、called
等,详见Mock。
总结 ¶
无论是pytest-mock这层薄薄的封装,还是unittest.mock本身,都还有很多未介绍的细节。 但以上介绍的内容,应该已经足够满足绝大部分使用场景。
在懂了mock之后,Python的单元测试功力,终于算是大成了。