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.argscall_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_valueside_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_argscall_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_callsmock_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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM