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
