Python内置库:unittest.mock(单元测试mock的基础使用)
1. 为什么需要使用mock
unittest.mock是用于在单元测试中模拟和替换指定的对象及行为,以便测试用例更加准确地进行测试运行。例如对于以下代码,想要针对函数func_a写一个简单的单元测试:
import unittest
def func_c(arg1, arg2):
a_dict = {}
# 其他代码
return a_dict
def func_b(arg3, arg4):
b_list = []
a_arg1 = None
a_arg2 = None
# 其他代码
a_dict = func_c(a_arg1, a_arg2)
# 其他代码
return b_list
def func_a():
b_list = func_b('111', '222')
if 'aaa' in b_list:
return False
return True
class FuncTest(unittest.TestCase):
def test_func_a(self):
assert func_a()
但是这样的话,函数func_b和func_c的逻辑都需要一起测试,在单元测试中这明显是不合理的,对于想要测试的函数func_a,里面所使用到的其他函数或接口,我们只需要关心它的返回值即可,保证当前测试的函数按它自己的逻辑运行,所以可以写成下面这样:
import unittest
def mock_func_b(arg3, arg4):
return ['bbb', 'ccc']
def func_a():
# 使用一个模拟的mock_func_b代替真正的函数func_b
# 这个mock_func_b不需要关心具体实现逻辑,只关心返回值
b_list = mock_func_b('111', '222')
if 'aaa' in b_list:
return False
return True
class FuncTest(unittest.TestCase):
def test_func_a(self):
assert func_a()
注意,模拟的mock_func_b并不需要保证func_a中所有的可能分支和逻辑都执行一次,单元测试更多的是验证函数或接口(比如这里的func_a)是否与设计相符、发现代码实现与需求中存在的错误、修改代码时是否引入了新的错误等。但是这里的写法也有很大的问题,一个功能模块中使用的函数或接口通常来讲其实并不少、也没有这里这么简单,如果涉及的接口都要重新写一个mock对象(如mock_func_b),那单元测试的工作将会变得非常繁重和复杂,所以unittest中的mock模块派上了用场,这个模块也正如它的名称一样,可以模拟各种对象。
import unittest
from unittest import mock
def func_a():
# 创建一个mock对象,return_value表示在该对象被执行时返回指定的值
mock_func_b = mock.Mock(return_value=['bbb', 'ccc'])
b_list = mock_func_b('111', '222')
if 'aaa' in b_list:
return False
return True
class FuncTest(unittest.TestCase):
def test_func_a(self):
assert func_a()
2. Mock对象
2.1 快速上手
mock模块中的Mock类最常用的就是Mock和MagicMock,可以用来模拟对象、属性和方法,并且会保存这些被模拟的对象的使用细节,之后再使用断言来判断它们是否按照期待的被使用。
使用Mock类指定其被调用时触发的一些行为(Mock对象也可以用于替换指定的对象或方法)。
>>> from unittest.mock import MagicMock, Mock
>>> mock = Mock(side_effect=KeyError('foo'))
>>> mock() # 直接调用将发生指定的异常
Traceback (most recent call last):
...
KeyError: 'foo'
>>> values = {'a': 1, 'b': 2, 'c': 3}
>>> def side_effect_func(arg):
... return values[arg]
...
>>> mock.side_effect = side_effect_func # 重新指定side_effect
>>> mock('a'), mock('b'), mock('c') # 表示只能传入指定的参数
(1, 2, 3)
>>> mock('a'), mock('b'), mock('c'), mock('d') # 传入未指定的参数则会报错
Traceback (most recent call last):
...
KeyError: 'd'
>>> mock.side_effect = [5, 4, 3, 2, 1] # 重新指定side_effect
>>> mock(), mock(), mock(), mock() # 相当于迭代器,依次返回对应的值,使用完后再次调用就会报错
(5, 4, 3, 2)
>>> mock()
1
>>> mock()
Traceback (most recent call last):
...
StopIteration
使用spec参数指定Mock对象的属性和方法,指定时可以是一个对象,会自动将该对象的属性和方法赋给当前Mock对象,但是注意赋值的属性和方法也是Mock类型的,并不会真正执行对应方法的内容。
from unittest.mock import MagicMock, Mock
class SpecMock:
def test_spec(self):
print('spec running...')
def test_mock_spec():
mock = Mock(spec=SpecMock())
print(mock.test_spec) # 注意打印的内容,返回的是一个Mock类型
print(mock.test_spec()) # 该方法内的内容并没有被执行
mock.func()
if __name__ == '__main__':
test_mock_spec()
'''输出:
<Mock name='mock.test_spec' id='1956426692808'>
<Mock name='mock.test_spec()' id='1956430210952'>
Traceback (most recent call last):
...
AttributeError: Mock object has no attribute 'func'
'''
使用MagicMock创建并替换原有的方法。
from unittest.mock import MagicMock
class TestClass:
def func(self, a, b):
return a + b
tc = TestClass()
# 使用MagicMock创建并替换原来的func方法,并指定其被调用时的返回值
tc.func = MagicMock(return_value='666')
print(tc.func(2, 3))
# 判断func是否按照指定的方式被调用,如果没有,
# 比如这里指定assert_called_with(4, 5),就会抛出异常,
# 因为之前使用的是tc.func(2, 3)来进行调用的
print(tc.func.assert_called_with(2, 3))
'''输出:
666
None
'''
Mock类虽然支持对Python中所有的magic方法进行“mock”,并允许给magic方法赋予其他的函数或者Mock实例,但是如果需要使用到magic方法,最简单的方式是使用MagicMock类,它继承自Mock并实现了所有常用的magic方法。
>>> from unittest.mock import MagicMock, Mock, patch
>>> mock = Mock()
>>> mock.__str__ = Mock(return_value='666')
>>> str(mock)
'666'
>>> m_mock = MagicMock()
>>> m_mock.__str__.return_value = '999'
>>> str(m_mock)
'999'
>>> m_mock.__str__.assert_called_with()
可以使用create_autospec函数来创建所有和原对象一样的api。
>>> from unittest.mock import create_autospec
>>> def func(a, b, c):
... pass
...
>>> mock_func = create_autospec(func, return_value='func autospec...')
>>> func(1, 2, 3)
>>> mock_func(1, 2, 3)
'func autospec...'
>>> mock_func(111)
Traceback (most recent call last):
...
TypeError: missing a required argument: 'b'
2.2 Mock类和MagicMock类
Mock对象可以用来模拟对象、属性和方法,Mock对象也会记录自身被使用的过程,你可以通过相关assert方法来测试验证代码是否被执行过。MagicMock类是Mock类的一个子类,它实现了所有常用的magic方法。
2.2.1 Mock构造函数
构造函数 unittest.mock.Mock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, unsafe=False, **kwargs)
参数解释:
- spec: 可以传入一个字符串列表、类或者实例,如果传入的是类或者实例对象,那么将会使用
dir
方法将该类或实例转化为一个字符串列表(magic属性和方法除外)。访问(get操作)任何不在此列表中的属性和方法时都会抛出AttributeError。如果传入的是一个类或者实例对象,那么__class__方法会返回对应的类,以便在使用isinstance
方法时进行判断。 - spec_set: spec参数的变体,但更加严格,如果试图使用get操作或set操作来操作此参数指定的对象中没有的属性或方法,则会抛出AttributeError。spec参数是可以对spec指定对象中没有的属性进行set操作的。参考
mock_add_spec
方法。 - side_effect: 可以传入一个函数,每次当Mock对象被调用的时候,就会自动调用该函数,可以用于抛出异常或者动态改变mock对象的返回值,此函数使用的参数与mock对象被调用时传入的参数是一样的,并且,除非它的返回值为
unittest.mock.DEFAULT
对象,否则这个函数的返回值将会作为mock对象的返回值。也可以传入一个exception对象或者实例对象,如果传入exception对象,则每次调用mock对象都会抛出该异常。也可以传入一个可迭代对象,每次调用mock对象时就会返回该迭代对象的下一个值。如果不想使用了,可以将它设置为None。具体参见后面mock对象side_effect
属性的使用。 - return_value: 每次调用mock对象时的返回值,默认第一次调用时创建新的Mock对象。
- unsafe: 如果某个属性或方法中会assert一个AttributeError,则可以设置
unsafe=True
来跳过这个异常。(Python3.5更新) - wraps: 包裹Mock对象的对象,当wraps不为None时,会将Mock对象的调用传入wraps对象中,并且可以通过Mock对象访问wraps对象中的属性。但是如果Mock对象指定了明确的return_value那么wraps对象就不会起作用了。
- name: 指定mock对象的名称,可在debug的时候使用,并且可以“传播”到子类中。
- 注: 初始化Mock对象时,还可以传入其他任意的关键字参数,这些参数会被用于设置成Mock对象的属性,具体参见后面的
configure_mock()
。
2.2.2 常用方法
assert_called()
assert:mock对象至少被调用过一次。(Python3.6新增)
assert_called_once()
assert:mock对象只被调用过一次。(Python3.6新增)
assert_called_with(*args, **kwargs)
assert:mock对象最后一次被调用的方式。
>>> from unittest.mock import Mock
>>> mock = Mock()
>>> mock.method(1, 2, 3, test='wow')
<Mock name='mock.method()' id='2956280756552'>
>>> mock.method.assert_called_with(1, 2, 3, test='wow')
assert_called_once_with(*args, **kwargs)
assert:mock对象以指定方式只被调用过一次。
assert_any_call(*args, **kwargs)
assert:mock对象以指定方式被调用过。
assert_has_calls(calls, any_order=False)
calls是一个 unittest.mock.call
对象列表,any_order默认为False,表示calls中的对象必须按照原来的调用顺序传入,为True则表示可以是任意顺序。
assert:mock对象以calls中指定的调用方式被调用过。
from unittest.mock import Mock, call
>>> mock = Mock(return_value=None)
>>> mock(1)
>>> mock(2)
>>> mock(3)
>>> mock(4)
>>> calls = [call(2), call(3)]
>>> mock.assert_has_calls(calls)
>>> calls = [call(4), call(2), call(3)]
>>> mock.assert_has_calls(calls, any_order=True)
assert_not_called()
assert:mock对象没有被调用过。(Python3.5新增)
reset_mock(*, return_value=False, side_effect=False)
重置所有调用相关的属性,但是默认不会改变它的return_value和side_effect,以及其他属性。
注:return_value和side_effect是两个关键字参数,并且是在Python3.6才增加的。
>>> from unittest.mock import Mock
>>> mock = Mock(return_value='hi')
>>> mock('hello')
'hi'
>>> mock.called
True
>>> mock.reset_mock()
>>> mock.called
False
mock_add_spec(spec, spec_set=False)
spec参数可以是一个对象或者一个字符串列表,如果指定了此参数,那么只有spec指定的属性才可以进行访问(get操作)。如果spec_set设置为True,那么只有spec中指定的属性才可以进行set操作。
>>> mock = Mock()
>>> mock.mock_add_spec(spec=['test_spec'])
>>> mock.test_spec
<Mock name='mock.test_spec' id='1504477311816'>
>>> mock.new_test_spec # 只能访问spec指定的属性
Traceback (most recent call last):
...
AttributeError: Mock object has no attribute 'new_test_spec'
>>> mock.new_test_spec = 'test spec!!!' # 但是可以设置新的属性
>>> mock.new_test_spec
'test spec!!!'
>>> mock.mock_add_spec(spec=['test_spec'], spec_set=True)
>>> mock.new_test_spec3 = 'test spec3' # spec_set设置为True后,将不能设置新的属性
Traceback (most recent call last):
...
AttributeError: Mock object has no attribute 'new_test_spec3'
attach_mock(mock, attribute)
将一个mock对象作为一个子属性添加到当前mock对象,并且会将其name值和parent关系进行替换。注意,此方法的调用会被记录在 method_calls
方法和 mock_calls
方法中。
configure_mock(**kwargs)
添加额外的属性到已经创建的mock对象,并且可以给属性添加return_value值和side_effect值。在创建mock对象时也可以用这种方式添加额外的属性。
>>> from unittest.mock import Mock
>>> mock = Mock()
>>> attrs = {'func.return_value': 'hello', 'side_func.side_effect': ValueError}
>>> mock.configure_mock(**attrs) # 给已经创建的mock对象添加额外的属性
>>> mock.func()
'hello'
>>> mock.side_func()
Traceback (most recent call last):
...
ValueError
>>> new_mock = Mock(other_attr='hi', **attrs) # 在创建mock对象时指定额外的属性,效果同configure_mock()方法
>>> new_mock.other_attr
'hi'
>>> new_mock.func()
'hello'
>>> new_mock.side_func()
Traceback (most recent call last):
...
ValueError
called
如果mock对象被调用过则返回True,否则返回False。
>>> mock = Mock(return_value=None)
>>> mock.called
False
>>> mock()
>>> mock.called
True
call_count
返回mock对象被调用的次数。
>>> mock = Mock(return_value=None)
>>> mock.call_count
0
>>> mock()
>>> mock()
>>> mock.call_count
2
return_value
指定mock对象被调用时的返回值,也可以在创建mock对象时通过参数进行指定。如果没有进行指定,return_value的默认值为一个mock对象,而且它就是一个正常的mock对象,你可以把它当成普通的mock对象进行其他操作。
>>> mock = Mock(return_value='hello')
>>> mock()
'hello'
>>> mock.return_value = 'hi'
>>> mock()
'hi'
>>> new_mock = Mock()
>>> new_mock.return_value
<Mock name='mock()' id='2064061578056'>
side_effect
这个属性可以是函数、可迭代对象或者异常(类或实例都可以),当mock对象被调用时, side_effect
属性对应的对象就会被调用一次。
如果传入的是函数,那么它将在mock对象调用时被执行,且执行时此函数传入的参数与mock对象被调用时的参数是一致的,此函数的返回值即mock被对象调用的返回值,但是如果函数的返回值是 unittest.mock.DEFAULT
对象,那么mock对象被调用的返回值就是它自身的return_value属性值。
如果传入的是一个可迭代对象,那么这个对象将被用作产生一个迭代器,这个迭代器在每一次mock对象被调用时返回一个值,这个值可以是异常类的实例,也可以是一个普通的值,当然如果这个返回值是一个 unittest.mock.DEFAULT
对象,则返回mock对象本身的return_value属性值。
side_effect
是一个异常:
>>> from unittest.mock import Mock
>>> mock = Mock()
>>> mock.side_effect = ValueError('hello')
>>> mock()
Traceback (most recent call last):
...
ValueError: hello
side_effect
是一个可迭代对象:
>>> mock.side_effect = [1, 2, 3]
>>> mock()
1
>>> mock()
2
>>> mock()
3
>>> mock()
Traceback (most recent call last):
...
StopIteration
side_effect
是一个 unittest.mock.DEFAULT
:
>>> from unittest.mock import DEFAULT, Mock
>>> def side_func(*args, **kwargs):
... return DEFAULT
...
>>> mock = Mock(return_value='hi')
>>> mock.side_effect = side_func
>>> mock()
'hi'
创建mock对象时指定 side_effect
为一个函数:
>>> def side_func(value):
... return value ** 2
...
>>> mock = Mock(side_effect=side_func)
>>> mock(3)
9
将 side_effect
指定为None,即可清除该选项:
>>> mock = Mock(side_effect=KeyError, return_value=3)
>>> mock()
Traceback (most recent call last):
...
KeyError
>>> mock.side_effect = None
>>> mock()
3
call_args
返回mock对象最近一次被调用时的参数,如果没有被调用过,则为None。
也可以通过 call_args.args
和 call_args.kwargs
属性分别获取对应的参数。(Python3.8新增)
>>> mock = Mock(return_value='hello')
>>> print(mock.call_args)
None
>>> mock('aa', 'bb', hi='hi')
'hello'
>>> mock.call_args
call('aa', 'bb', hi='hi')
>>> isinstance(mock.call_args, tuple)
True
>>> mock.call_args == (('aa', 'bb'), {'hi': 'hi'})
True
call_args_list
存储mock对象调用的列表,列表元素为call对象,在没有被调用之前为空列表。
>>> from unittest.mock import Mock
>>> mock = Mock(return_value=None)
>>> mock.call_args_list
[]
>>> mock(1, 2)
>>> mock(arg1='hi', arg2='hello')
>>> mock.call_args_list
[call(1, 2), call(arg1='hi', arg2='hello')]
>>> mock.call_args_list == [((1, 2), ), ({'arg1': 'hi', 'arg2': 'hello'}, )]
True
method_calls
存储mock对象调用以及“调用的调用“的列表,列表元素为call对象,在没有被调用之前为空列表。
>>> mock = Mock()
>>> mock.method_calls
[]
>>> mock.func()
<Mock name='mock.func()' id='2152783337672'>
>>> mock.pro.func2.attr()
<Mock name='mock.pro.func2.attr()' id='2152784407496'>
>>> mock.method_calls
[call.func(), call.pro.func2.attr()]
mock_calls
存储mock对象所有类型调用的列表。
>>> from unittest.mock import call, Mock
>>> mock = Mock()
>>> mock(1, 2, 3)
<Mock name='mock()' id='2152784400584'>
>>> result = mock.func(a=3)
>>> result(44)
<Mock name='mock.func()()' id='2152771939848'>
>>> mock.top(a=3).bottom()
<Mock name='mock.top().bottom()' id='2152784434888'>
>>> mock.mock_calls
[call(1, 2, 3),
call.func(a=3),
call.func()(44),
call.top(a=3),
call.top().bottom()]
>>> mock.mock_calls[-1] == call.top(a=-1).bottom() # 子调用bottom是没有记录其父调用top的参数的
True
class
如果mock对象指定了spec对象,则会返回spec对象的类型,也可以直接赋值。这个属性主要是在 isinstance
进行判断的时候会用到。
>>> mock = Mock(spec=3)
>>> isinstance(mock, int)
True
>>> mock.__class__ = dict # 如果不想特别去指定spec参数,可以直接进行赋值
>>> isinstance(mock, dict)
True
2.3 其他Mock类
2.3.1 NonCallableMock类
unittest.mock.NonCallableMock
这是一个不可被调用的mock类,它的参数和Mock类的使用是一样的,不过 return_value
和 side_effect
这两个参数对 NonCallableMock
类来说是无意义的。
2.3.2 PropertyMock类
unittest.mock.PropertyMock
这是一个专门用于替换属性的Mock类,它提供了属性对应的get和set方法。
from unittest.mock import patch, PropertyMock
class Foo:
@property
def foo(self):
return 'something'
@foo.setter
def foo(self, value):
pass
# 使用PropertyMock替换foo属性进行测试
with patch('__main__.Foo.foo', new_callable=PropertyMock) as mock_foo:
mock_foo.return_value = 'mockity-mock'
this_foo = Foo()
print(this_foo.foo) # 调用foo的get方法
this_foo.foo = 6 # 调用foo的set方法
print(mock_foo.mock_calls)
'''输出:
mockity-mock
[call(), call(6)]
'''
2.3.3 AsyncMock类 (Python3.8新增)
unittest.mock.AsyncMock
一个MagicMock的异步版本,AsyncMock对象会像一个异步函数一样运行,它的调用的返回值是一个awaitable对象,这个awaitable对象返回 side_effect
或者 return_value
指定的值。
>>> import asyncio
>>> import inspect
>>> from unittest.mock import AsyncMock
>>> mock = AsyncMock()
>>> asyncio.iscoroutinefunction(mock)
True
>>> inspect.isawaitable(mock())
True
如果Mock或者MagicMock的spec参数指定了一个异步的函数,那么对应mock对象的调用将返回一个协程对象。
>>> from unittest.mock import MagicMock
>>> async def async_func(): pass # 注意async关键字是在Python3.7才有的
...
>>> mock = MagicMock(async_func)
>>> mock
<MagicMock spec='function' id='1934190100048'>
>>> mock()
<coroutine object AsyncMockMixin._execute_mock_call at 0x000001C2568E8EC0>
如果Mock、MagicMock或者AsyncMock的spec参数指定了带有同步或者异步函数的类,那么对于Mock,所有的同步函数将被定义为Mock对象,对于MagicMock和AsyncMock,所有同步函数将被定义为MagicMock。而对于Mock、MagicMock或者AsyncMock,所有的异步函数都将被定义为AsyncMock对象。
>>> class ExampleClass:
... def sync_foo():
... pass
... async def async_foo():
... pass
...
>>> a_mock = AsyncMock(ExampleClass)
>>> a_mock.sync_foo
<MagicMock name='mock.sync_foo' id='1934183952000'>
>>> a_mock.async_foo
<AsyncMock name='mock.async_foo' id='1934183974272'>
>>> from unittest.mock import Mock
>>> mock = Mock(ExampleClass)
>>> mock.sync_foo
<Mock name='mock.sync_foo' id='1934183980864'>
>>> mock.async_foo
<AsyncMock name='mock.async_foo' id='1934183978800'>
assert_awaited()
assert:mock对象至少被await过一次。注意,await的对象是被从mock对象中分离出来的,且该分离出来的对象必须被await关键字声明过才能进行assert判断。
>>> mock = AsyncMock()
>>> async def main(coroutine_mock):
... await coroutine_mock
...
>>> coroutine_mock = mock()
>>> mock.called
True
>>> mock.assert_awaited()
Traceback (most recent call last):
File "<input>", line 1, in <module>
...
AssertionError: Expected mock to have been awaited.
>>> asyncio.run(main(coroutine_mock))
>>> mock.assert_awaited()
assert_awaited_once()
assert:mock对象只被await了一次。
>>> mock = AsyncMock()
>>> async def main():
... await mock()
...
>>> asyncio.run(main())
>>> mock.assert_awaited_once()
>>> asyncio.run(main())
>>> mock.method.assert_awaited_once()
Traceback (most recent call last):
File "<input>", line 1, in <module>
...
AssertionError: Expected method to have been awaited once. Awaited 0 times.
assert_awaited_with(*args, **kwargs)
assert:mock对象最后一次的await的参数和指定的参数一致。
>>> mock = AsyncMock()
>>> async def main(*args, **kwargs):
... await mock(*args, **kwargs)
...
>>> asyncio.run(main('foo', bar='bar'))
>>> mock.assert_awaited_with('foo', bar='bar')
>>> mock.assert_awaited_with('other')
Traceback (most recent call last):
File "<input>", line 1, in <module>
...
AssertionError: expected await not found.
Expected: mock('other')
Actual: mock('foo', bar='bar')
assert_awaited_once_with(*args, **kwargs)
assert:mock对象只被await过一次,且使用的参数和指定的参数一致。
>>> mock = AsyncMock()
>>> async def main(*args, **kwargs):
... await mock(*args, **kwargs)
...
>>> asyncio.run(main('foo', bar='bar'))
>>> mock.assert_awaited_once_with('foo', bar='bar')
>>> asyncio.run(main('foo', bar='bar'))
>>> mock.assert_awaited_once_with('foo', bar='bar')
Traceback (most recent call last):
File "<input>", line 1, in <module>
...
AssertionError: Expected mock to have been awaited once. Awaited 2 times.
assert_any_await(*args, **kwargs)
assert:mock对象以指定的参数await过。
>>> mock = AsyncMock()
>>> async def main(*args, **kwargs):
... await mock(*args, **kwargs)
...
>>> asyncio.run(main('foo', bar='bar'))
>>> asyncio.run(main('hello'))
>>> mock.assert_any_await('foo', bar='bar')
>>> mock.assert_any_await('other')
Traceback (most recent call last):
File "<input>", line 1, in <module>
...
AssertionError: mock('other') await not found
assert_has_awaits(calls, any_order=False)
assert:mock对象以指定的call对象的调用方式await过。any_order用于指定是否需要判断call调用的顺序,默认需要判断。
>>> mock = AsyncMock()
>>> async def main(*args, **kwargs):
... await mock(*args, **kwargs)
...
>>> from unittest.mock import call
>>> calls = [call("foo"), call("bar")]
>>> mock.assert_has_awaits(calls)
Traceback (most recent call last):
File "<input>", line 1, in <module>
...
AssertionError: Awaits not found.
Expected: [call('foo'), call('bar')]
Actual: []
>>> asyncio.run(main('foo'))
>>> asyncio.run(main('bar'))
>>> mock.assert_has_awaits(calls)
assert_not_awaited()
assert:mock对象没有被await过。
reset_mock(*args, **kwargs)
与 Mock.reset_mock
使用相似,会将 await_count
置为0, await_args
置为None,清除 await_args_list
中的内容。
await_count
mock对象被await的次数。
await_args
mock对象最近一次被await的调用信息,是一个call对象。如果没有被await过,则为None。和 Mock.call_args
相似。
>>> mock = AsyncMock()
>>> async def main(*args):
... await mock(*args)
...
>>> mock.await_args
>>> asyncio.run(main('foo'))
>>> mock.await_args
... call('foo')
>>> asyncio.run(main('bar'))
>>> mock.await_args
call('bar')
await_args_list
是一个记录mock对象所有的await调用信息的列表,列表元素为call对象,初始值为空列表。
>>> mock = AsyncMock()
>>> async def main(*args):
... await mock(*args)
...
>>> asyncio.run(main('foo'))
>>> asyncio.run(main('bar'))
>>> mock.await_args_list
[call('foo'), call('bar')]
2.4 Calling
Mock对象每次调用都会返回 return_value
属性,默认的 return_value
是一个新的Mock对象,它会在第一次 return_value
被访问时创建,并且以后每次访问 return_value
都会返回第一次创建的Mock对象。
2.4.1 call_args和call_args_list
Mock的每次调用都会记录在 call_args
和 call_args_list
中。具体使用示例见之前的2.2.2章节。
2.4.2 side_effect属性
如果设置了 side_effect
属性,那么在调用时,会先记录此次调用信息,再去调用 side_effect
指定的对象。所以想要mock对象的调用抛出一个异常的最简单方式就是使用 side_effect
属性指定一个异常类或者异常实例。
>>> m = MagicMock(side_effect=IndexError)
>>> m(1, 2, 3)
...
IndexError
>>> m.mock_calls
[...
call(1, 2, 3),
...]
>>> m.side_effect = KeyError('Bang!')
>>> m('two', 'three', 'four')
Traceback (most recent call last):
...
KeyError: 'Bang!'
>>> m.mock_calls
[...
call(1, 2, 3),
...
call('two', 'three', 'four'),
...]
如果 side_effect
是一个函数,那么调用mock对象的时候就会使用相同的参数去调用此函数。
>>> m = MagicMock(side_effect=side_effect)
>>> m(1)
2
>>> m(2)
3
>>> m.mock_calls
[...,
call(1),
...,
call(2),
...]
如果想要mock对象的调用返回一个默认值,那么可以有以下两种方式:在 side_effect
指定的函数中直接返回 mock.return_value
,或者返回 DEFAULT
对象。
>>> from unittest.mock import DEFAULT
>>> m = MagicMock()
>>> # 方式一
>>> def side_effect(*args, **kwargs):
... return m.return_value
...
>>> m.side_effect = side_effect
>>> m.return_value = 3
>>> m()
3
>>> # 方式二
>>> def side_effect(*args, **kwargs):
... return DEFAULT
...
>>> m.side_effect = side_effect
>>> m()
3
如果想要移除 side_effect
并返回mock的默认值,将它设置为None就可以了。
>>> m = MagicMock(return_value=6)
>>> def side_effect(*args, **kwargs):
... return 3
...
>>> m.side_effect = side_effect
>>> m()
3
>>> m.side_effect = None
>>> m()
6
side_effect
的值也可以是可迭代对象,每次调用会依次获取可迭代对象中的下一个值,一直到可迭代对象的末尾,并触发StopIteration异常。如果可迭代对象中含有异常,当迭代到此异常时将会抛出该异常。
>>> iterable = (33, ValueError, 66)
>>> m = MagicMock(side_effect=iterable)
>>> m()
33
>>> m()
Traceback (most recent call last):
File "<input>", line 1, in <module>
...
ValueError
>>> m()
66
>>> m()
Traceback (most recent call last):
File "<input>", line 1, in <module>
...
StopIteration
2.4.3 name属性
如果想要设置mock对象的name属性,可以有两种方式:使用 mock.configure_mock(name='my_name')
,或者直接给mock对象赋值 mock.name='my_name'
。
如果mock对象的属性是另一个mock对象时,这个属性的mock就相当于是父mock的子mock,子mock的调用会被记录在父mock的 method_calls
和 mock_calls
中,如果你不想子mock的调用被记录,则可以在定义子mock时指定name属性,指定了name属性的子mock则不会被记录在父mock中。
>>> parent = MagicMock()
>>> child1 = MagicMock(return_value=None)
>>> child2 = MagicMock(return_value=None)
>>> parent.child1 = child1
>>> parent.child2 = child2
>>> child3 = MagicMock(name='child3')
>>> parent.child3 = child3
>>> child1(1)
>>> child2(2)
>>> child3(3)
<MagicMock name='child3()' id='2247991039888'>
>>> parent.mock_calls
[...,
call.child1(1),
...,
call.child2(2),
...]
如果需要将一个含有name属性的子mock对象赋给父mock,且可以记录子mock的调用,则需要使用attach_mock方法来将子mock赋给父mock。
thing1 = object()
thing2 = object()
parent = MagicMock()
with patch('__main__.thing1', return_value=None) as child1:
with patch('__main__.thing2', return_value=None) as child2:
# attach_mock第一个参数是mock对象,第二个参数是属性名,将mock对象当作属性赋给父mock对象
parent.attach_mock(child1, 'child1')
parent.attach_mock(child2, 'child2')
child1('one')
child2('two')
print(parent.mock_calls)
'''输出为
[call.child1('one'), call.child2('two')]
'''
3. patch使用
from unittest.mock import patch
可以用装饰器的方式对属性、方法和类进行装饰,或者在with上下文中使用,或者使用start和stop方法直接在代码中使用。使用patch的目的是在代码运行时将指定的对象变为执行mock对象,并且是在单元测试开始时就可以指定所有的mock对象,非常方便。
3.1 快速上手
可以使用patch装饰器替换某个模块的类,但是注意,导入时需要使用import导入对应的模块,也只能到模块这一级,函数中传参的顺序也必须是与装饰的顺序一致(从下到上)。
# 只能导入到模块(文件和包)这一级,不能直接导入类
# 这里的unittest_mock包下有一个test文件,本示例中对应的类都定义在这个文件中
import unittest_mock.test
# patch使用时传入对应类的路径字符串
@patch('unittest_mock.test.PatchTest2')
@patch('unittest_mock.test.PatchTest1')
def patch_test(MockTest1, MockTest2): # 注意这里的传参顺序是按照装饰的顺序(从下到上)来指定的
unittest_mock.test.PatchTest1() # 这里执行的已经不是真实的类了,而是一个MagicMock类
unittest_mock.test.PatchTest2()
assert MockTest1 is unittest_mock.test.PatchTest1 # 这里表明传入的参数和对应的类是相同的,都是MagicMock类
assert MockTest2 is unittest_mock.test.PatchTest2
assert MockTest1.called # 表明这个类在这之前已经被调用了
assert MockTest2.called
if __name__ == '__main__':
patch_test()
可以使用with语法来使用 patch.object
装饰器。
class PatchObjTest:
def func(self, a, b, c):
print(a, b, c)
def test_patch_obj():
with patch.object(PatchObjTest, 'func',
return_value='mock obj func...') as mock_func:
patch_obj = PatchObjTest()
print(patch_obj.func(1, 2, 3))
mock_func.assert_called_once_with(1, 2, 3)
if __name__ == '__main__':
test_patch_obj()
'''输出:
mock obj func...
'''
可以会使用 patch.dict
替换原有的字典对象。
def test_patch_dict():
foo = {'key': 'value'}
original = foo.copy() # 浅拷贝
# clear参数表示是否保留原有的项,True表示不保留, 默认保留
with patch.dict(foo, {'new_key': 'new_value'}, clear=True):
print(foo)
assert foo == {'new_key': 'new_value'}
print(foo) # foo原本的值并没有被改变
assert foo == original
if __name__ == '__main__':
test_patch_dict()
'''输出:
{'new_key': 'new_value'}
{'key': 'value'}
'''
3.2 patch使用
unittest.mock.patch
可以作为一个函数装饰器,类装饰器,或者上下文管理器(with语句)。
3.2.1 构造函数
构造函数 unittest.mock.patch(target, new=DEFAULT, spec=None, create=False, spec_set=None, autospec=None, new_callable=None, **kwargs)
参数解释:
- target:target参数是一个形如
package.module.ClassName
的字符串。target值将会被import并创建一个新的对象,所以target字符串必须是在当前环境可以import的。需要注意,被装饰的函数执行时,target的对象才会被创建,而不是运行装饰器的时候被创建。 - new:如果没有指定,则对于async函数会创建一个AsyncMock对象,对于其他的,则会创建一个MagicMock对象。如果
patch()
是作为一个装饰器,且new参数没有指定,则创建的mock对象将会作为一个额外(即放在被装饰函数原有的参数之后)的参数传入被装饰的函数。如果patch()
用在上下文管理器中,则创建的mock对象会被上下文管理器返回。 - spec和spec_set:会当作参数传入MagicMock中。如果创建的是spec或spec_set对象,可以设置spec=True或者spec_set=True,以便让patch正常运行。
- new_callable:可以是一个类或者一个callable对象,并会使用此参数创建一个对象,默认情况下,对于async函数会创建一个AsyncMock对象,对于其他的,则会创建一个MagicMock对象。
- create:默认为False,如果指定为True,那么当patch的对象或函数不存在时会自动创建,当真正的对象在运行过程中被程序创建后就删除patch出来的mock对象,这个参数特别适用于一些运行时创建的内容。(Python3.5更新:如果想要patch的内容是
builtin
内建模块,则不用指定create=True
,patch会在运行时自动创建。)
3.2.2 基础使用
patch可以作为一个装饰器为函数创建一个mock对象并传入被装饰的函数。如果patch装饰的是一个类,那么将会返回一个MagicMock对象,当这个类在test方法中被实例化时,那么将会返回此MagicMock对象的 return_value
值,注意,如果在一个test方法中实例化多次,也是返回的同一个对象,如果想要每次都返回新的不同的对象,那么可以使用 side_effect
参数。
class SomeClass:
pass
@patch('__main__.SomeClass')
def func(a, b, mock_someclass):
print(a)
print(b)
print(mock_someclass)
if __name__ == '__main__':
func(2, 3)
'''打印输出
2
3
<MagicMock name='SomeClass' id='1519607444288'>
'''
如果mock了一个类,对该类的实例对象和真实的class进行 isinstance
判断,则需要指定 spec=True
。
class Class:
def method(self):
pass
def func():
Original = Class
patcher = patch('__main__.Class', spec=True)
MockClass = patcher.start()
instance = MockClass()
# 如果不指定spec=True,则会抛出异常
assert isinstance(instance, Original)
patcher.stop()
if __name__ == '__main__':
func()
patch默认创建的是MagicMock对象,如果想要创建一个指定的对象,就可以使用 new_callable
参数。甚至可以使用 new_callable
参数在test case中重定向输出。
thing = object()
with patch('__main__.thing', new_callable=NonCallableMock) as mock_thing:
assert thing is mock_thing
thing()
'''打印输出
Traceback (most recent call last):
...
TypeError: 'NonCallableMock' object is not callable
'''
from io import StringIO
def foo():
print('Something')
@patch('sys.stdout', new_callable=StringIO)
def test(mock_stdout):
foo()
assert mock_stdout.getvalue() == 'Something\n'
test()
patch中可以通过传参的方式给mock对象设置属性。
>>> patcher = patch('__main__.thing', first='one', second='two')
>>> mock_thing = patcher.start()
>>> mock_thing.first
'one'
>>> mock_thing.second
'two'
可以通过字典的方式来配置mock对象的属性。
>>> config = {'method.return_value': 3, 'other.side_effect': KeyError}
>>> patcher = patch('__main__.thing', **config)
>>> mock_thing = patcher.start()
>>> mock_thing.method()
3
>>> mock_thing.other()
Traceback (most recent call last):
...
KeyError
3.3 patch其他使用
3.3.1 patch.object
patch.object用来给对象(target参数)的成员(attribute参数)进行“mock”,其参数的用法和patch是一样的,且也可以使用参数的形式给创建的mock对象添加额外的属性。如果被装饰的对象是类的话,可以使用 patch.TEST_PREFIX
指定哪些方法需要被“mock”。
patch.object被用来装饰一个函数的时候,那么被创建的mock对象会一个额外参数的形式传入被装饰的函数。
@patch.object(SomeClass, 'class_method')
def test(mock_method):
SomeClass.class_method(3)
mock_method.assert_called_with(3)
test()
3.3.2 patch.dict
patch.dict
用来“mock”一个字典对象或者类似字典的对象,int_dict
参数为需要“mock”的字典对象,也可以是一个可以通过import生成字典对象的字符串,values参数为创建的字典对象的内容,也可以是(key, value)形式的键值对。当test case结束后,原先的被mock的字典对象就会恢复。
# 示例:直接mock字典对象
foo = {}
@patch.dict(foo, {'newkey': 'newvalue'})
def test():
assert foo == {'newkey': 'newvalue'}
test()
assert foo == {}
# 示例:在类中mock字典对象
import os
import unittest
from unittest.mock import patch
@patch.dict('os.environ', {'newkey': 'newvalue'})
class TestSample(unittest.TestCase):
def test_sample(self):
self.assertEqual(os.environ['newkey'], 'newvalue')
# 示例:修改原本的字典对象
foo = {}
with patch.dict(foo, {'newkey': 'newvalue'}) as patched_foo:
assert foo == {'newkey': 'newvalue'}
assert patched_foo == {'newkey': 'newvalue'}
# 可以往mock的字典中添加、删除、修改内容,当with上下文结束后,原先的foo就会恢复
patched_foo['spam'] = 'eggs'
assert foo == {}
assert patched_foo == {}
# 示例:mock内置模块的类似字典的对象
import os
with patch.dict('os.environ', {'newkey': 'newvalue'}):
print(os.environ['newkey'])
assert 'newkey' not in os.environ
可以使用参数配置的方式给字典对象添加内容。
mymodule = MagicMock()
mymodule.function.return_value = 'fish'
with patch.dict('sys.modules', mymodule=mymodule):
import mymodule
print(mymodule.function('some', 'args'))
patch.dict
也支持一些类似字典但不是字典类型的对象,但是这些对象必须具有以下Magic方法: __getitem__()
, __setitem__()
, __delitem__()
,以及 __iter__()
和 __contains__()
中的一个。
class Container:
def __init__(self):
self.values = {}
def __getitem__(self, name):
return self.values[name]
def __setitem__(self, name, value):
self.values[name] = value
def __delitem__(self, name):
del self.values[name]
def __iter__(self):
return iter(self.values)
thing = Container()
thing['one'] = 1
with patch.dict(thing, one=2, two=3):
assert thing['one'] == 2
assert thing['two'] == 3
assert thing['one'] == 1
assert list(thing) == ['one']
3.3.3 patch.multiple
patch.multiple
可以一次性创建多个mock对象,参数的用法和patch是一样的。
使用patch.multiple创建多个mock对象时,需要使用 DEFAULT
对象。
thing = object()
other = object()
# from unittest.mock import DEFAULT
@patch.multiple('__main__', thing=DEFAULT, other=DEFAULT)
def test_function(thing, other): # 对于patch.multiple对应的参数,并没有特别顺序要求
assert isinstance(thing, MagicMock)
assert isinstance(other, MagicMock)
test_function()
也可以和patch作为装饰器一起使用,但是 patch.multiple
产生的额外参数传入被装饰的函数时需要放在patch的参数后面。
thing = object()
other = object()
@patch('sys.exit')
@patch.multiple('__main__', thing=DEFAULT, other=DEFAULT)
def test_function(mock_exit, other, thing): # 注意传入参数的顺序,other和thing必须在mock_exit后面,但是other和thing之间的顺序无所谓
assert 'other' in repr(other)
assert 'thing' in repr(thing)
assert 'exit' in repr(mock_exit)
test_function()
如果 patch.multiple
在with中使用,则with返回的是一个字典对象。
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
3.3.4 patch的start和stop方法
如果不想使用装饰器或with语法而直接使用patch,那么可以使用patch的start方法和stop方法。start方法能直接返回对应的mock对象,而stop方法则是取消使用patch,类似with语句的开始和结束。
patcher = patch('package.module.ClassName')
from package import module
original = module.ClassName
new_mock = patcher.start()
assert module.ClassName is not original
assert module.ClassName is new_mock
patcher.stop()
assert module.ClassName is original
assert module.ClassName is not new_mock
使用start和stop方法的另一个典型例子是test case的setUp和tearDown方法。
class MyTest(unittest.TestCase):
def setUp(self):
self.patcher1 = patch('package.module.Class1')
self.patcher2 = patch('package.module.Class2')
self.MockClass1 = self.patcher1.start()
self.MockClass2 = self.patcher2.start()
def tearDown(self):
self.patcher1.stop()
self.patcher2.stop()
def test_something(self):
assert package.module.Class1 is self.MockClass1
assert package.module.Class2 is self.MockClass2
MyTest('test_something').run()
调用了start后一定要记得调用stop,也可以在最后使用stopall方法一次性stop所有使用了start方法的patch对象。如果怕自己在最后忘记了调用stop方法,也可以在调用了start方法后,立即调用 unittest.TestCase.addCleanup()
方法,此方法会在最后自动调用stop。
class MyTest(unittest.TestCase):
def setUp(self):
patcher = patch('package.module.Class')
self.MockClass = patcher.start()
self.addCleanup(patcher.stop)
def test_something(self):
assert package.module.Class is self.MockClass
注: 此学习笔记大多是直接从官方文档翻译过来的https://docs.python.org/3/library/unittest.mock.html