unittest.mock --- 入門指南¶
在 3.3 版被加入.
使用 Mock 的方式¶
使用 Mock 來 patching 方法¶
Mock 物件的常見用法包含:
Patching 方法
記錄在物件上的方法呼叫
你可能會想要取代一個物件上的方法,以便檢查系統的另一部分是否使用正確的引數呼叫它:
>>> real = SomeClass()
>>> real.method = MagicMock(name='method')
>>> real.method(3, 4, 5, key='value')
<MagicMock name='method()' id='...'>
一旦我們的 mock 已經被使用(例如在這個範例中的 real.method),它就有了方法和屬性,允許你對其使用方式進行斷言 (assertions)。
一旦 mock 被呼叫,它的 called 屬性將被設定為 True。更重要的是,我們可以使用 assert_called_with() 或 assert_called_once_with() 方法來檢查它是否被使用正確的引數來呼叫。
這個範例測試呼叫 ProductionClass().method 是否導致對 something 方法的呼叫:
>>> class ProductionClass:
... def method(self):
... self.something(1, 2, 3)
... def something(self, a, b, c):
... pass
...
>>> real = ProductionClass()
>>> real.something = MagicMock()
>>> real.method()
>>> real.something.assert_called_once_with(1, 2, 3)
對物件的方法呼叫使用 mock¶
在上一個範例中,我們直接對物件上的方法進行 patch,以檢查它是否被正確呼叫。另一個常見的用法是將一個物件傳遞給一個方法(或受測系統的某一部分),然後檢查它是否以正確的方式被使用。
下面是一個單純的 ProductionClass,含有一個 closer 方法。如果它被傳入一個物件,它就會呼叫此物件中的 close。
>>> class ProductionClass:
... def closer(self, something):
... something.close()
...
因此,為了對此進行測試,我們需要傳遞一個具有 close 方法的物件,並檢查它是否被正確的呼叫。
>>> real = ProductionClass()
>>> mock = Mock()
>>> real.closer(mock)
>>> mock.close.assert_called_with()
我們不必做任何額外的事情來為 mock 提供 'close' 方法,存取 close 會建立它。因此,如果 'close' 並未被呼叫過,在測試中存取 'close' 就會建立它,但 assert_called_with() 就會引發一個失敗的例外。
Mock 類別¶
一個常見的使用案例是在測試的時候 mock 被程式碼實例化的類別。當你 patch 一個類別時,該類別就會被替換為 mock。實例是透過呼叫類別建立的。這代表你可以透過查看被 mock 的類別的回傳值來存取「mock 實例」。
在下面的範例中,我們有一個函式 some_function,它實例化 Foo 並呼叫它的方法。對 patch() 的呼叫將類別 Foo 替換為一個 mock。Foo 實例是呼叫 mock 的結果,因此它是透過修改 mock return_value 來配置的。:
>>> def some_function():
... instance = module.Foo()
... return instance.method()
...
>>> with patch('module.Foo') as mock:
... instance = mock.return_value
... instance.method.return_value = 'the result'
... result = some_function()
... assert result == 'the result'
命名你的 mock¶
為你的 mock 命名可能會很有用。這個名稱會顯示在 mock 的 repr 中,且當 mock 出現在測試的失敗訊息中時,名稱會很有幫助。該名稱也會傳播到 mock 的屬性或方法:
>>> mock = MagicMock(name='foo')
>>> mock
<MagicMock name='foo' id='...'>
>>> mock.method
<MagicMock name='foo.method' id='...'>
追蹤所有呼叫¶
通常你會想要追蹤對一個方法的多個呼叫。mock_calls 屬性記錄對 mock 的子屬性以及其子屬性的所有呼叫。
>>> mock = MagicMock()
>>> mock.method()
<MagicMock name='mock.method()' id='...'>
>>> mock.attribute.method(10, x=53)
<MagicMock name='mock.attribute.method()' id='...'>
>>> mock.mock_calls
[call.method(), call.attribute.method(10, x=53)]
如果你對 mock_calls 做出斷言並且有任何不預期的方法被呼叫,則斷言將失敗。這很有用,因為除了斷言你期望的呼叫已經進行之外,你還可以檢查它們是否按正確的順序進行,並且沒有多餘的呼叫:
你可以使用 call 物件來建構串列以與 mock_calls 進行比較:
>>> expected = [call.method(), call.attribute.method(10, x=53)]
>>> mock.mock_calls == expected
True
然而,回傳 mock 的呼叫的參數不會被記錄,這代表在巢狀呼叫中,無法追蹤用於建立上代的參數 important 的值:
>>> m = Mock()
>>> m.factory(important=True).deliver()
<Mock name='mock.factory().deliver()' id='...'>
>>> m.mock_calls[-1] == call.factory(important=False).deliver()
True
設定回傳值和屬性¶
在 mock 物件上設定回傳值非常簡單:
>>> mock = Mock()
>>> mock.return_value = 3
>>> mock()
3
當然,你可以對 mock 上的方法執行相同的操作:
>>> mock = Mock()
>>> mock.method.return_value = 3
>>> mock.method()
3
回傳值也可以在建構函式中設定:
>>> mock = Mock(return_value=3)
>>> mock()
3
如果你需要在 mock 上進行屬性設置,只需執行以下操作:
>>> mock = Mock()
>>> mock.x = 3
>>> mock.x
3
有時你想要 mock 更複雜的情況,例如 mock.connection.cursor().execute("SELECT 1")。如果我們希望此呼叫回傳一個串列,那麼我們就必須配置巢狀呼叫的結果。
如下所示,我們可以使用 call 在「鍊接呼叫 (chained call)」中建構呼叫的集合,以便在之後輕鬆的進行斷言:
>>> mock = Mock()
>>> cursor = mock.connection.cursor.return_value
>>> cursor.execute.return_value = ['foo']
>>> mock.connection.cursor().execute("SELECT 1")
['foo']
>>> expected = call.connection.cursor().execute("SELECT 1").call_list()
>>> mock.mock_calls
[call.connection.cursor(), call.connection.cursor().execute('SELECT 1')]
>>> mock.mock_calls == expected
True
正是對 .call_list() 的呼叫將我們的呼叫物件轉換為代表鍊接呼叫的呼叫串列。
透過 mock 引發例外¶
一個有用的屬性是 side_effect。如果將其設定為例外類別或實例,則當 mock 被呼叫時將引發例外。
>>> mock = Mock(side_effect=Exception('Boom!'))
>>> mock()
Traceback (most recent call last):
...
Exception: Boom!
Side effect 函式以及可疊代物件¶
side_effect 也可以設定為函式或可疊代物件。side_effect 作為可疊代物件的使用案例是:當你的 mock 將會被多次呼叫,且你希望每次呼叫回傳不同的值。當你將 side_effect 設定為可疊代物件時,對 mock 的每次呼叫都會傳回可疊代物件中的下一個值:
>>> mock = MagicMock(side_effect=[4, 5, 6])
>>> mock()
4
>>> mock()
5
>>> mock()
6
對於更進階的使用案例,例如根據 mock 被呼叫的內容動態變更回傳值,可以將 side_effect 設成一個函式。該函式會使用與 mock 相同的引數被呼叫。函式回傳的內容就會是呼叫回傳的內容:
>>> vals = {(1, 2): 1, (2, 3): 2}
>>> def side_effect(*args):
... return vals[args]
...
>>> mock = MagicMock(side_effect=side_effect)
>>> mock(1, 2)
1
>>> mock(2, 3)
2
Mock 非同步可疊代物件¶
從 Python 3.8 開始,AsyncMock 和 MagicMock 支援透過 __aiter__ 來 mock Asynchronous Iterators。__aiter__ 的 return_value 屬性可用來設定用於疊代的回傳值。
>>> mock = MagicMock() # AsyncMock also works here
>>> mock.__aiter__.return_value = [1, 2, 3]
>>> async def main():
... return [i async for i in mock]
...
>>> asyncio.run(main())
[1, 2, 3]
Mock 非同步情境管理器¶
從 Python 3.8 開始,AsyncMock 和 MagicMock 支援透過 __aenter__ 和 __aexit__ 來 mock Asynchronous Context Managers。預設情況下,__aenter__ 和 __aexit__ 是回傳非同步函式的 AsyncMock 實例。
>>> class AsyncContextManager:
... async def __aenter__(self):
... return self
... async def __aexit__(self, exc_type, exc, tb):
... pass
...
>>> mock_instance = MagicMock(AsyncContextManager()) # AsyncMock also works here
>>> async def main():
... async with mock_instance as result:
... pass
...
>>> asyncio.run(main())
>>> mock_instance.__aenter__.assert_awaited_once()
>>> mock_instance.__aexit__.assert_awaited_once()
從現有物件建立 mock¶
過度使用 mock 的一個問題是,它將你的測試與 mock 的實作結合在一起,而不是與真實的程式碼結合。假設你有一個實作 some_method 的類別,在另一個類別的測試中,你提供了一個 mock 的物件,其也提供了 some_method。如果之後你重構第一個類別,使其不再具有 some_method - 那麼即使你的程式碼已經損壞,你的測試也將繼續通過!
Mock 允許你使用 spec 關鍵字引數提供一個物件作為 mock 的規格。對 mock 存取規格物件上不存在的方法或屬性將立即引發一個屬性錯誤。如果你更改規格的實作,那麼使用該類別的測試將立即失敗,而無需在這些測試中實例化該類別。
>>> mock = Mock(spec=SomeClass)
>>> mock.old_method()
Traceback (most recent call last):
...
AttributeError: Mock object has no attribute 'old_method'. Did you mean: 'class_method'?
使用規格還可以更聰明地匹配對 mock 的呼叫,無論引數是作為位置引數還是命名引數傳遞:
>>> def f(a, b, c): pass
...
>>> mock = Mock(spec=f)
>>> mock(1, 2, 3)
<Mock name='mock()' id='140161580456576'>
>>> mock.assert_called_with(a=1, b=2, c=3)
如果你希望這種更聰明的匹配也可以應用於 mock 上的方法呼叫,你可以使用自動規格。
如果你想要一種更強大的規格形式來防止設定任意屬性以及取得它們,那麼你可以使用 spec_set 而不是 spec。
使用 side_effect 回傳各別檔案內容¶
mock_open() 是用於 patch open() 方法。side_effect 可以用來在每次呼叫回傳一個新的 mock 物件。這可以用於回傳儲存在字典中的各別檔案的不同內容:
DEFAULT = "default"
data_dict = {"file1": "data1",
"file2": "data2"}
def open_side_effect(name):
return mock_open(read_data=data_dict.