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: object has no attribute 'old_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.get(name, DEFAULT))()
with patch("builtins.open", side_effect=open_side_effect):
with open("file1") as file1:
assert file1.read() == "data1"
with open("file2") as file2:
assert file2.read() == "data2"
with open("file3") as file2:
assert file2.read() == "default"
Patch 裝飾器¶
備註
使用 patch()
時,需注意的是你得在被查找物件的命名空間中(in the namespace where they are looked up)patch 物件。這通常很直接,但若需要快速導引,請參閱該 patch 何處。
測試中的常見需求是 patch 類別屬性或模組屬性,例如 patch 一個內建函式(built-in)或 patch 模組中的類別以測試它是否已實例化。模組和類別實際上是全域的,因此在測試後必須撤銷它們的 patch,否則 patch 將延續到其他測試中並導致難以診斷問題。
mock 為此提供了三個方便的裝飾器:patch()
、patch.object()
和 patch.dict()
。patch
接受單一字串,格式為 package.module.Class.attribute
,用來指定要 patch 的屬性。同時它也可以接受你想要替換的屬性(或類別或其他)的值。patch.object
接受一個物件和你想要 patch 的屬性的名稱,同時也可以接受要 patch 的值。
patch.object
:
>>> original = SomeClass.attribute
>>> @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test():
... assert SomeClass.attribute == sentinel.attribute
...
>>> test()
>>> assert SomeClass.attribute == original
>>> @patch('package.module.attribute', sentinel.attribute)
... def test():
... from package.module import attribute
... assert attribute is sentinel.attribute
...
>>> test()
如果你要 patch 一個模組(包括 builtins
),請使用 patch()
而非 patch.object()
:
>>> mock = MagicMock(return_value=sentinel.file_handle)
>>> with patch('builtins.open', mock):
... handle = open('filename', 'r')
...
>>> mock.assert_called_with('filename', 'r')
>>> assert handle == sentinel.file_handle, "incorrect file handle returned"
如有需要,模組名稱可以含有 .
,形式為 package.module
:
>>> @patch('package.module.ClassName.attribute', sentinel.attribute)
... def test():
... from package.module import ClassName
... assert ClassName.attribute == sentinel.attribute
...
>>> test()
一個好的模式是實際裝飾測試方法本身:
>>> class MyTest(unittest.TestCase):
... @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test_something(self):
... self.assertEqual(SomeClass.attribute, sentinel.attribute)
...
>>> original = SomeClass.attribute
>>> MyTest('test_something').test_something()
>>> assert SomeClass.attribute == original
如果你想使用一個 mock 進行 patch,你可以使用僅帶有一個引數的 patch()
(或帶有兩個引數的 patch.object()
)。Mock 將被建立並被傳遞到測試函式 / 方法中:
>>> class MyTest(unittest.TestCase):
... @patch.object(SomeClass, 'static_method')
... def test_something(self, mock_method):
... SomeClass.static_method()
... mock_method.assert_called_with()
...
>>> MyTest('test_something').test_something()
你可以使用這個模式堆疊多個 patch 裝飾器:
>>> class MyTest(unittest.TestCase):
... @patch('package.module.ClassName1')
... @patch('package.module.ClassName2')
... def test_something(self, MockClass2, MockClass1):
... self.assertIs(package.module.ClassName1, MockClass1)
... self.assertIs(package.module.ClassName2, MockClass2)
...
>>> MyTest('test_something').test_something()
當你巢狀使用 patch 裝飾器時,mock 傳遞到被裝飾函式的順序會跟其被應用的順序相同(一般 Python 應用裝飾器的順序)。這意味著由下而上,因此在上面的範例中,module.ClassName2
的 mock 會先被傳入。
也有 patch.dict()
,用於在測試範圍中設定字典內的值,並在測試結束時將其恢復為原始狀態:
>>> foo = {'key': 'value'}
>>> original = foo.copy()
>>> with patch.dict(foo, {'newkey': 'newvalue'}, clear=True):
... assert foo == {'newkey': 'newvalue'}
...
>>> assert foo == original
patch
、patch.object
和 patch.dict
都可以用來作為情境管理器。
當你使用 patch()
為你建立一個 mock 時,你可以使用 with 陳述式的 "as" 形式來取得 mock 的參照:
>>> class ProductionClass:
... def method(self):
... pass
...
>>> with patch.object(ProductionClass, 'method') as mock_method:
... mock_method.return_value = None
... real = ProductionClass()
... real.method(1, 2, 3)
...
>>> mock_method.assert_called_with(1, 2, 3)
另外,“patch”、“patch.object” 和 “patch.dict” 也可以用來作為類別裝飾器。以這種方式使用時,與將裝飾器單獨應用於每個名稱以 “test” 開頭的方法相同。
更多範例¶
以下是一些更進階一點的情境的範例。
Mock 鍊接呼叫¶
一旦你了解了 return_value
屬性,mock 鍊接呼叫其實就很簡單了。當一個 mock 第一次被呼叫,或者你在它被呼叫之前取得其 return_value
時,一個新的 Mock
就會被建立。
這代表你可以透過查問 return_value
mock 來了解一個對被 mock 的物件的呼叫回傳的物件是如何被使用的:
>>> mock = Mock()
>>> mock().foo(a=2, b=3)
<Mock name='mock().foo()' id='...'>
>>> mock.return_value.foo.assert_called_with(a=2, b=3)
這裡只需一個簡單的步驟即可進行配置並對鍊接呼叫進行斷言。當然,另一種選擇是先以更容易被測試的方式撰寫程式碼...
所以,假設我們有一些程式碼,看起來大概像這樣:
>>> class Something:
... def __init__(self):
... self.backend = BackendProvider()
... def method(self):
... response = self.backend.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
... # more code
假設 BackendProvider
已經經過充分測試,那麼我們該如何測試 method()
?具體來說,我們要測試程式碼部分 # more code
是否以正確的方式使用 response
物件。
由於此呼叫鍊是從實例屬性進行的,因此我們可以在 Something
實例上 monkey patch backend
屬性。在這種特定的情況下,我們只對最終呼叫 start_call
的回傳值感興趣,因此我們不需要做太多配置。我們假設它傳回的物件是類檔案物件 (file-like),因此我們會確保我們的 response 物件使用內建的 open()
作為其 spec
。
為此,我們建立一個 mock 實例作為我們的 mock backend,並為其建立一個 mock response 物件。要將 response 設定為最後的 start_call
的回傳值,我們可以這樣做:
mock_backend.get_endpoint.return_value.create_call.return_value.start_call.return_value = mock_response
我們可以使用 configure_mock()
方法來以稍微好一點的方式為我們直接設定回傳值:
>>> something = Something()
>>> mock_response = Mock(spec=open)
>>> mock_backend = Mock()
>>> config = {'get_endpoint.return_value.create_call.return_value.start_call.return_value': mock_response}
>>> mock_backend.configure_mock(**config)
有了這些,我們就可以原地 (in place) monkey patch "mock backend",並且可以進行真正的呼叫:
>>> something.backend = mock_backend
>>> something.method()
藉由使用 mock_calls
,我們可以使用單一一個斷言來檢查鍊接呼叫。一個鍊接呼叫是一行程式碼中的多個呼叫,因此 mock_calls
中會有多個項目。我們可以使用 call.call_list()
來為我們建立這個呼叫串列:
>>> chained = call.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
>>> call_list = chained.call_list()
>>> assert mock_backend.mock_calls == call_list
部分 mocking¶
在某些測試中,我們會想 mock 對 datetime.date.today()
的呼叫以回傳一個已知日期,但我不想阻止測試中的程式碼建立新的日期物件。不幸的是 datetime.date
是用 C 語言寫的,所以們我不能 monkey patch 靜態的 datetime.date.today()
方法。
我們找到了一種簡單的方法來做到這一點,其用 mock 有效地包裝日期類別,但將對建構函式的呼叫傳遞給真實的類別(並返回真實的實例)。
這裡使用 patch 裝飾器
來 mock 被測模組中的 date
類別。然後,mock 日期類別上的 side_effect
屬性會被設定為回傳真實日期的 lambda 函式。當 mock 日期類別被呼叫時,將透過 side_effect
建構並回傳真實日期。:
>>> from datetime import date
>>> with patch('mymodule.date') as mock_date:
... mock_date.today.return_value = date(2010, 10, 8)
... mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
...
... assert mymodule.date.today() == date(2010, 10, 8)
... assert mymodule.date(2009, 6, 8) == date(2009, 6, 8)
注意,我們沒有全域 patch datetime.date
,而是在使用它的模組中 patch date
。請參閱 該 patch 何處。
當 date.today()
被呼叫時,一個已知日期會被回傳,但對 date(...)
建構函式的呼叫仍然會回傳正常日期。如果不這樣使用,你可能會發現自己必須使用與被測程式碼完全相同的演算法來計算預期結果,這是一個典型的測試的反面模式 (anti-pattern)。
對日期建構函式的呼叫被記錄在 mock_date
屬性(call_count
及其相關屬性)中,這對你的測試也可能有用處。
處理 mock 日期或其他內建類別的另一種方法在 這個 blog 中討論。
Mock 產生器方法¶
Python 產生器是一個函式或方法,它使用 yield
陳述式在疊代 [1] 時回傳一系列的值。
產生器方法 / 函式會被呼叫以回傳產生器物件。之後此產生器會進行疊代。疊代的協定方法是 __iter__()
,所以我們可以使用 MagicMock
來 mock 它。
下面是一個範例類別,其包含實作產生器的一個 "iter" 方法:
>>> class Foo:
... def iter(self):
... for i in [1, 2, 3]:
... yield i
...
>>> foo = Foo()
>>> list(foo.iter())
[1, 2, 3]
我們該如何 mock 這個類別,特別是它的 "iter" 方法呢?
要配置從疊代回傳的值(隱含在對 list
的呼叫中),我們需要配置呼叫 foo.iter()
所回傳的物件。
>>> mock_foo = MagicMock()
>>> mock_foo.iter.return_value = iter([1, 2, 3])
>>> list(mock_foo.iter())
[1, 2, 3]
對每個測試方法應用相同的 patch¶
如果你希望 patch 能用在多個測試方法上,顯而易見的方式是將 patch 裝飾器應用於每個方法。這感覺是不必要的重複行為,因此你可以使用 patch()
(及其他 patch 的變體)作為類別裝飾器。這會將 patch 應用在該類別的所有測試方法上。測試方法由名稱以 test
開頭來識別:
>>> @patch('mymodule.SomeClass')
... class MyTest(unittest.TestCase):
...
... def test_one(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def test_two(self, MockSomeClass):
... self.assertIs(mymodule.SomeClass, MockSomeClass)
...
... def not_a_test(self):
... return 'something'
...
>>> MyTest('test_one').test_one()
>>> MyTest('test_two').test_two()
>>> MyTest('test_two').not_a_test()
'something'
管理 patch 的另一種方式是使用 patch 方法:啟動與停止。這允許你將 patch 移到你的 setUp
與 tearDown
方法中。:
>>> class MyTest(unittest.TestCase):
... def setUp(self):
... self.patcher = patch('mymodule.foo')
... self.mock_foo = self.patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
... def tearDown(self):
... self.patcher.stop()
...
>>> MyTest('test_foo').run()
如果你使用這個技巧,你必須確保透過呼叫 stop
來 "取消" patch。這可能會比你想像的還要複雜一點,因為如果有例外在 setUp
中被引發,則 tearDown
就不會被呼叫。unittest.TestCase.addCleanup()
會讓這稍微簡單一點:
>>> class MyTest(unittest.TestCase):
... def setUp(self):
... patcher = patch('mymodule.foo')
... self.addCleanup(patcher.stop)
... self.mock_foo = patcher.start()
...
... def test_foo(self):
... self.assertIs(mymodule.foo, self.mock_foo)
...
>>> MyTest('test_foo').run()
Mock Unbound Methods (未繫結方法)¶
在撰寫測試時,當我們需要 patch 一個未繫結方法(patch 類別上的方法而不是實例上的方法)。我們需要將 self 作為第一個引數傳入,因為我們想斷言哪些物件正在呼叫這個特定方法。問題是你無法為此使用 mock 進行 patch,因為就算你用一個 mock 替換未繫結方法,從實例取得它時它也不會成為一個繫結方法,因此 self 並不會被傳遞。解決方法是使用真實的函式來 patch 未繫結方法。patch()
裝飾器使得用 mock 來 patch out 方法是如此的簡單,以至於建立一個真正的函式相對變得很麻煩。
如果你將 autospec=True
傳遞給 patch,那麼它會使用真的函式物件來進行 patch。此函式物件與它所替換的函式物件具有相同的簽名,但實際上委託給 mock。你仍然會以與之前完全相同的方式自動建立 mock。但這意味著,如果你使用它來 patch 類別上的未繫結方法,則從實例取得的 mock 函式將轉換為繫結方法。self
會作為其第一個引數傳入,而這正是我們想要的:
>>> class Foo:
... def foo(self):
... pass
...
>>> with patch.object(Foo, 'foo', autospec=True) as mock_foo:
... mock_foo.return_value = 'foo'
... foo = Foo()
... foo.foo()
...
'foo'
>>> mock_foo.assert_called_once_with(foo)
如果我們不使用 autospec=True
,那麼未繫結方法將使用一個 Mock 實例 patch out,並且不被使用 self
進行呼叫。
使用 mock 檢查多個呼叫¶
mock 有很好的 API,用於對 mock 物件的使用方式做出斷言。
>>> mock = Mock()
>>> mock.foo_bar.return_value = None
>>> mock.foo_bar('baz', spam='eggs')
>>> mock.foo_bar.assert_called_with('baz', spam='eggs')
如果你的 mock 只被呼叫一次,你可以使用 assert_called_once_with()
方法,其也斷言 call_count
是1。
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
>>> mock.foo_bar()
>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
Traceback (most recent call last):
...
AssertionError: Expected to be called once. Called 2 times.
assert_called_with
和 assert_called_once_with
都對最近一次的呼叫做出斷言。如果你的 mock 將被多次呼叫,並且你想要對所有這些呼叫進行斷言,你可以使用 call_args_list
:
>>> mock = Mock(return_value=None)
>>> mock(1, 2, 3)
>>> mock(4, 5, 6)
>>> mock()
>>> mock.call_args_list
[call(1, 2, 3), call(4, 5, 6), call()]
call
輔助函式可以輕鬆地對這些呼叫做出斷言。你可以建立預期呼叫的清單並將其與 call_args_list
進行比較。這看起來與 call_args_list
的 repr 非常相似:
>>> expected = [call(1, 2, 3), call(4, 5, 6), call()]
>>> mock.call_args_list == expected
True
應對可變引數¶
另一種情況很少見,但可能會困擾你,那就是當你的 mock 被使用可變引數呼叫。call_args
和 call_args_list
儲存對引數的參照。如果引數被測試中的程式碼改變,那麼你將無法再對 mock 被呼叫時的值進行斷言。
這是一些秀出問題的程式碼範例。 想像 'mymodule' 中定義以下函式:
def frob(val):
pass
def grob(val):
"First frob and then clear val"
frob(val)
val.clear()
當我們嘗試測試 grob
使用正確的引數呼叫 frob
時,看看會發生什麼:
>>> with patch('mymodule.frob') as mock_frob:
... val = {6}
... mymodule.grob(val)
...
>>> val
set()
>>> mock_frob.assert_called_with({6})
Traceback (most recent call last):
...
AssertionError: Expected: (({6},), {})
Called with: ((set(),), {})
一種可能是讓 mock 複製你傳入的引數。如果你進行的斷言依賴於物件識別性來確定相等性,這就可能導致問題。
以下是一種使用 side_effect
功能的解法。如果你為 mock 提供一個 side_effect
函式,則 side_effect
將被使用與 mock 相同的引數呼叫。這使我們有機會複製引數並將其儲存以供之後的斷言。在這個範例中,我們使用另一個 mock 來儲存引數,以便我們可以使用 mock 方法來執行斷言。同樣的,有一個輔助函式為我們設定了這些。:
>>> from copy import deepcopy
>>> from unittest.mock import Mock, patch, DEFAULT
>>> def copy_call_args(mock):
... new_mock = Mock()
... def side_effect(*args, **kwargs):
... args = deepcopy(args)
... kwargs = deepcopy(kwargs)
... new_mock(*args, **kwargs)
... return DEFAULT
... mock.side_effect = side_effect
... return new_mock
...
>>> with patch('mymodule.frob') as mock_frob:
... new_mock = copy_call_args(mock_frob)
... val = {6}
... mymodule.grob(val)
...
>>> new_mock.assert_called_with({6})
>>> new_mock.call_args
call({6})
copy_call_args
與將要被呼叫的 mock 一起被呼叫。它回傳一個我們會對其進行斷言的新的 mock。side_effect
函式建立引數們的副本,並用該副本呼叫我們的 new_mock
。
備註
如果你的 mock 只會被使用一次,則有一種更簡單的方法可以在呼叫引數時檢查它們。你可以簡單地在 side_effect
函式內進行檢查。
>>> def side_effect(arg):
... assert arg == {6}
...
>>> mock = Mock(side_effect=side_effect)
>>> mock({6})
>>> mock(set())
Traceback (most recent call last):
...
AssertionError
另一種方法是建立 Mock
或 MagicMock
的子類別來複製(使用 copy.deepcopy()
)引數。這是一個實作的例子:
>>> from copy import deepcopy
>>> class CopyingMock(MagicMock):
... def __call__(self, /, *args, **kwargs):
... args = deepcopy(args)
... kwargs = deepcopy(kwargs)
... return super().__call__(*args, **kwargs)
...
>>> c = CopyingMock(return_value=None)
>>> arg = set()
>>> c(arg)
>>> arg.add(1)
>>> c.assert_called_with(set())
>>> c.assert_called_with(arg)
Traceback (most recent call last):
...
AssertionError: Expected call: mock({1})
Actual call: mock(set())
>>> c.foo
<CopyingMock name='mock.foo' id='...'>
當你將 Mock
或 MagicMock
子類別化時,所有屬性會被動態建立,且 return_value
會自動使用你的子類別。這代表著 CopyingMock
的所有子代 (child) 也會具有 CopyingMock
型別。
巢狀使用 Patch¶
將 patch 作為情境管理器使用很好,但是如果你使用複數個 patch,你最終可能會得到巢狀的 with 陳述式,並且越來越向右縮排:
>>> class MyTest(unittest.TestCase):
...
... def test_foo(self):
... with patch('mymodule.Foo') as mock_foo:
... with patch('mymodule.Bar') as mock_bar:
... with patch('mymodule.Spam') as mock_spam:
... assert mymodule.Foo is mock_foo
... assert mymodule.Bar is mock_bar
... assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').test_foo()
>>> assert mymodule.Foo is original
我們可以使用 unittest 的 cleanup
函式以及 patch 方法:啟動與停止 來達到相同的效果,而不會出現因巢狀導致的縮排。一個簡單的輔助方法 create_patch
會將 patch 放置到位並為我們回傳被建立的 mock:
>>> class MyTest(unittest.TestCase):
...
... def create_patch(self, name):
... patcher = patch(name)
... thing = patcher.start()
... self.addCleanup(patcher.stop)
... return thing
...
... def test_foo(self):
... mock_foo = self.create_patch('mymodule.Foo')
... mock_bar = self.create_patch('mymodule.Bar')
... mock_spam = self.create_patch('mymodule.Spam')
...
... assert mymodule.Foo is mock_foo
... assert mymodule.Bar is mock_bar
... assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').run()
>>> assert mymodule.Foo is original
使用 MagicMock 來 mock 字典¶
你可能會想要 mock 字典或其他容器物件,記錄對它的所有存取,同時讓它仍然像字典一樣運作。
我們可以使用 MagicMock
來做到這一點,它的行為會與字典一致,並使用 side_effect
將字典存取委託給我們控制下的真實底層字典。
當 MagicMock
的 __getitem__()
和 __setitem__()
方法被呼叫時(一般的字典存取), side_effect
會被使用鍵 (key) 來呼叫 (在 __setitem__
的情況也會使用值 (value))。我們也可以控制回傳的內容。
使用 MagicMock
後,我們可以使用諸如 call_args_list
之類的屬性來斷言字典被使用的方式:
>>> my_dict = {'a': 1, 'b': 2, 'c': 3}
>>> def getitem(name):
... return my_dict[name]
...
>>> def setitem(name, val):
... my_dict[name] = val
...
>>> mock = MagicMock()
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem
備註
不使用 MagicMock
的替代方案是使用 Mock
並且只提供你特別想要的魔術方法:
>>> mock = Mock()
>>> mock.__getitem__ = Mock(side_effect=getitem)
>>> mock.__setitem__ = Mock(side_effect=setitem)
第三個選擇是使用 MagicMock
,但傳入 dict
作為 spec (或 spec_set)引數,以使被建立的 MagicMock
僅具有字典可用的魔術方法:
>>> mock = MagicMock(spec_set=dict)
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem
有了這些 side effect 函式,mock
會像一般的字典一樣運作,但會記錄存取。如果你嘗試存取不存在的鍵,它甚至會引發一個 KeyError
。
>>> mock['a']
1
>>> mock['c']
3
>>> mock['d']
Traceback (most recent call last):
...
KeyError: 'd'
>>> mock['b'] = 'fish'
>>> mock['d'] = 'eggs'
>>> mock['b']
'fish'
>>> mock['d']
'eggs'
在上述方式被使用後,你就可以使用普通的 mock 方法和屬性對存取進行斷言:
>>> mock.__getitem__.call_args_list
[call('a'), call('c'), call('d'), call('b'), call('d')]
>>> mock.__setitem__.call_args_list
[call('b', 'fish'), call('d', 'eggs')]
>>> my_dict
{'a': 1, 'b': 'fish', 'c': 3, 'd': 'eggs'}
Mock 子類別及其屬性¶
你會想子類別化 Mock
的原因可能有很多種。其中之一是增加輔助方法。以下是一個有點笨的例子:
>>> class MyMock(MagicMock):
... def has_been_called(self):
... return self.called
...
>>> mymock = MyMock(return_value=None)
>>> mymock
<MyMock id='...'>
>>> mymock.has_been_called()
False
>>> mymock()
>>> mymock.has_been_called()
True
Mock
實例的標準行為是屬性 mock 和回傳值 mock 會與存取它們的 mock 具有相同的型別。這確保了 Mock
屬性是 Mocks
,MagicMock
屬性是 MagicMocks
[2]。因此,如果你要子類別化以新增輔助方法,那麼它們也可用於子類別實例的屬性 mock 和回傳值 mock。
>>> mymock.foo
<MyMock name='mock.foo' id='...'>
>>> mymock.foo.has_been_called()
False
>>> mymock.foo()
<MyMock name='mock.foo()' id='...'>
>>> mymock.foo.has_been_called()
True
有時候這很不方便。例如,有一個使用者正在子類別化 mock 以建立 Twisted adaptor。將其應用於屬性實際上會導致錯誤。
Mock
(及其各種形式)使用名為 _get_child_mock
的方法來為屬性和回傳值建立這些 "子 mock"。你可以透過置換此方法來防止你的子類別被用為屬性。其簽名是取用任意的關鍵字引數(**kwargs
),然後將其傳遞給 mock 建構函式:
>>> class Subclass(MagicMock):
... def _get_child_mock(self, /, **kwargs):
... return MagicMock(**kwargs)
...
>>> mymock = Subclass()
>>> mymock.foo
<MagicMock name='mock.foo' id='...'>
>>> assert isinstance(mymock, Subclass)
>>> assert not isinstance(mymock.foo, Subclass)
>>> assert not isinstance(mymock(), Subclass)
此規則的例外是非可呼叫物件的 mock。屬性使用可呼叫物件的變體,否則非可呼叫物件的 mock 無法具有可呼叫的方法。
使用 patch.dict 來 mock import¶
可能會讓 mock 很困難的一種情況是在函式內部進行區域 import。這些狀況會更難進行 mock,因為它們沒有使用我們可以 patch out 的模組命名空間中的物件。
一般來說,我們應該避免區域 import 的發生。有時這樣做是為了防止循環相依 (circular dependencies),為此通常有更好的方式來解決問題(例如重構程式碼)或透過延遲 import 來防止「前期成本 (up front costs)」。這也可以透過比無條件的區域 import 更好的方式來解決(例如將模組儲存為類別或模組屬性,並且僅在第一次使用時進行 import)。
除此之外,還有一種方法可以使用 mock
來影響 import 的結果。Import 會從 sys.modules
字典中取得一個物件。請注意,它會取得一個物件,而該物件不需要是一個模組。初次 import 模組會導致模組物件被放入 sys.modules
中,因此通常當你 import 某些東西時,你會得到一個模組。但並非一定要如此。
這代表你可以使用 patch.dict()
來暫時在 sys.modules
中原地放置 mock。在這個 patch 作用時的任何 import 都會取得這個 mock。當 patch 完成時(被裝飾的函式結束、with 陳述式主體完成或 patcher.stop()
被呼叫),那麼之前在 sys.modules
中的任何內容都會被安全地復原。
下面是一個 mock out 'fooble' 模組的例子。
>>> import sys
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
... import fooble
... fooble.blob()
...
<Mock name='mock.blob()' id='...'>
>>> assert 'fooble' not in sys.modules
>>> mock.blob.assert_called_once_with()
如你所見,import fooble
成功了,但在離開之後,sys.modules
中就沒有 'fooble' 了。
這也適用於 from module import name
形式:
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
... from fooble import blob
... blob.blip()
...
<Mock name='mock.blob.blip()' id='...'>
>>> mock.blob.blip.assert_called_once_with()
透過稍微多一點的處理,你也可以 mock 套件的引入:
>>> mock = Mock()
>>> modules = {'package': mock, 'package.module': mock.module}
>>> with patch.dict('sys.modules', modules):
... from package.module import fooble
... fooble()
...
<Mock name='mock.module.fooble()' id='...'>
>>> mock.module.fooble.assert_called_once_with()
追蹤呼叫順序與更簡潔的呼叫斷言¶
Mock
類別可以讓你透過 method_calls
屬性追蹤 mock 物件上方法呼叫的順序。這不會讓你可以追蹤不同的 mock 物件之間的呼叫順序,然而我們可以使用 mock_calls
來達到相同的效果。
因為 mock 會在 mock_calls
中追蹤對子 mock 的呼叫,且存取 mock 的任意屬性會建立一個子 mock,所以我們可以從父 mock 建立不同的 mock。之後對這些子 mock 的呼叫將依序全部記錄在父 mock 的 mock_calls
中:
>>> manager = Mock()
>>> mock_foo = manager.foo
>>> mock_bar = manager.bar
>>> mock_foo.something()
<Mock name='mock.foo.something()' id='...'>
>>> mock_bar.other.thing()
<Mock name='mock.bar.other.thing()' id='...'>
>>> manager.mock_calls
[call.foo.something(), call.bar.other.thing()]
之後我們可以透過與管理器 (manager) mock 上的 mock_calls
屬性進行比較來斷言呼叫及其順序:
>>> expected_calls = [call.foo.something(), call.bar.other.thing()]
>>> manager.mock_calls == expected_calls
True
如果 patch
正在被建立並放置為你的 mock,那麼你可以使用 attach_mock()
方法將它們附加到管理器 mock。附加之後,呼叫將被記錄在管理器的 mock_calls
中。:
>>> manager = MagicMock()
>>> with patch('mymodule.Class1') as MockClass1:
... with patch('mymodule.Class2') as MockClass2:
... manager.attach_mock(MockClass1, 'MockClass1')
... manager.attach_mock(MockClass2, 'MockClass2')
... MockClass1().foo()
... MockClass2().bar()
<MagicMock name='mock.MockClass1().foo()' id='...'>
<MagicMock name='mock.MockClass2().bar()' id='...'>
>>> manager.mock_calls
[call.MockClass1(),
call.MockClass1().foo(),
call.MockClass2(),
call.MockClass2().bar()]
如果進行了多次呼叫,但你只對其中特定呼叫的序列感興趣,則可以使用 assert_has_calls()
方法。這需要一個呼叫串列(使用 call
物件建構)。如果該呼叫序列位於 mock_calls
中,則斷言成功。
>>> m = MagicMock()
>>> m().foo().bar().baz()
<MagicMock name='mock().foo().bar().baz()' id='...'>
>>> m.one().two().three()
<MagicMock name='mock.one().two().three()' id='...'>
>>> calls = call.one().two().three().call_list()
>>> m.assert_has_calls(calls)
儘管鍊接呼叫 m.one().two().three()
並不是對 mock 進行的唯一呼叫,斷言仍會成功。
有時可能會對一個 mock 進行多次呼叫,而你只對斷言其中某些呼叫感興趣。你甚至可能不關心順序。在這種情況下,你可以將 any_order=True
傳遞給 assert_has_calls
:
>>> m = MagicMock()
>>> m(1), m.two(2, 3), m.seven(7), m.fifty('50')
(...)
>>> calls = [call.fifty('50'), call(1), call.seven(7)]
>>> m.assert_has_calls(calls, any_order=True)
更複雜的引數匹配¶
使用與 ANY
相同的基本概念,我們可以實作匹配器 (matcher),對用來作為 mock 引數的物件進行更複雜的斷言。
假設我們預期某個物件會被傳進 mock,預設情況下,該 mock 會根據物件識別性進行相等比較(對使用者定義類別的 Python 預設行為)。要使用 assert_called_with()
,我們需要傳入完全相同的物件。如果我們只對該物件的某些屬性感興趣,那麼我們可以建立一個匹配器來為我們檢查這些屬性。
你可以在這個範例中看到對 assert_called_with
的一個「標準」呼叫是不夠的:
>>> class Foo:
... def __init__(self, a, b):
... self.a, self.b = a, b
...
>>> mock = Mock(return_value=None)
>>> mock(Foo(1, 2))
>>> mock.assert_called_with(Foo(1, 2))
Traceback (most recent call last):
...
AssertionError: Expected: call(<__main__.Foo object at 0x...>)
Actual call: call(<__main__.Foo object at 0x...>)
對我們的 Foo
類別的比較函式看起來可能會像這樣:
>>> def compare(self, other):
... if not type(self) == type(other):
... return False
... if self.a != other.a:
... return False
... if self.b != other.b:
... return False
... return True
...
而可以使用像這樣的比較函式進行其相等性運算的匹配器物件會長得像這樣:
>>> class Matcher:
... def __init__(self, compare, some_obj):
... self.compare = compare
... self.some_obj = some_obj
... def __eq__(self, other):
... return self.compare(self.some_obj, other)
...
把這些都放在一起:
>>> match_foo = Matcher(compare, Foo(1, 2))
>>> mock.assert_called_with(match_foo)
Matcher
是用我們想要比較的 Foo
物件和比較函式實例化的。在 assert_called_with
中,Matcher
相等方法將被呼叫,它將呼叫 mock 時傳入的物件與我們建立匹配器時傳入的物件進行比較。如果它們匹配,則 assert_called_with
會通過,如果它們不匹配,則會引發 AssertionError
:
>>> match_wrong = Matcher(compare, Foo(3, 4))
>>> mock.assert_called_with(match_wrong)
Traceback (most recent call last):
...
AssertionError: Expected: ((<Matcher object at 0x...>,), {})
Called with: ((<Foo object at 0x...>,), {})
透過一些調整,你可以讓比較函式直接引發 AssertionError
並提供更有用的失敗訊息。
從版本 1.5 開始,Python 測試函式庫 PyHamcrest 以其相等匹配器的形式,提供了類似的功能,在這裡可能是有用的 (hamcrest.library.integration.match_equality)。