unittest.mock
— 入門¶
在 3.3 版本加入。
使用 Mock¶
Mock 修補方法¶
Mock
物件的常見用途包括:
修補方法
記錄物件上的方法呼叫
你可能希望替換物件上的方法,以檢查系統其他部分是否使用正確的引數呼叫了它。
>>> real = SomeClass()
>>> real.method = MagicMock(name='method')
>>> real.method(3, 4, 5, key='value')
<MagicMock name='method()' id='...'>
一旦我們的 mock 被使用(本例中為 real.method
),它就擁有允許你斷言其使用方式的方法和屬性。
一旦 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 物件上的方法呼叫¶
在上一個示例中,我們直接修補了物件上的方法以檢查它是否被正確呼叫。另一個常見用例是將物件傳遞給方法(或被測系統的某個部分),然後檢查它是否以正確的方式被使用。
下面簡單的 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”尚未被呼叫,那麼在測試中訪問它將建立它,但 assert_called_with()
將引發失敗異常。
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 的呼叫的引數不會被記錄,這意味著無法跟蹤祖先建立時使用的引數很重要的巢狀呼叫。
>>> 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.connection.cursor().execute("SELECT 1")
。如果希望此呼叫返回一個列表,則必須配置巢狀呼叫的結果。
我們可以使用 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
作為可迭代物件的用例是當你的 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
模擬非同步迭代器¶
從 Python 3.8 開始,AsyncMock
和 MagicMock
支援透過 __aiter__
模擬非同步迭代器。__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]
模擬非同步上下文管理器¶
從 Python 3.8 開始,AsyncMock
和 MagicMock
支援透過 __aenter__
和 __aexit__
模擬非同步上下文管理器。預設情況下,__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¶
過度使用模擬的一個問題是它將你的測試與模擬的實現而非你的真實程式碼耦合起來。假設你有一個實現了 some_method
的類。在另一個類的測試中,你提供了一個模擬此物件的 mock,該 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()
用於修補 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"
修補裝飾器¶
測試中的常見需求是修補類屬性或模組屬性,例如修補內建函式或修補模組中的類以測試其是否被例項化。模組和類實際上是全域性的,因此修補它們後必須在測試後撤銷,否則修補將持續到其他測試中並導致難以診斷的問題。
mock 提供了三個方便的裝飾器:patch()
、patch.object()
和 patch.dict()
。patch
接受一個字串,形式為 package.module.Class.attribute
,以指定要修補的屬性。它還可以選擇接受一個值,你希望用該值替換該屬性(或類或任何其他內容)。‘patch.object’ 接受一個物件和要修補的屬性名稱,以及可選的用於修補的值。
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()
如果你正在修補模組(包括 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.object()
和兩個引數)使用 patch()
。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 應用裝飾器的正常順序)傳遞到被裝飾的函式中。這意味著從下到上,所以在上面的示例中,test_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”開頭的方法相同。
更多示例¶
這裡有一些更高階場景的示例。
模擬鏈式呼叫¶
一旦你理解了 return_value
屬性,使用 mock 模擬鏈式呼叫實際上很簡單。當 mock 第一次被呼叫,或者你在它被呼叫之前獲取它的 return_value
時,會建立一個新的 Mock
。
這意味著你可以透過查詢 return_value
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
是否以正確的方式使用了響應物件。
由於這個呼叫鏈是由一個例項屬性組成的,我們可以對 Something
例項上的 backend
屬性進行猴子補丁。在這種特殊情況下,我們只對最後一次呼叫 start_call
的返回值感興趣,因此我們沒有太多配置要做。假設它返回的物件是“檔案類”的,所以我們將確保我們的響應物件使用內建的 open()
作為其 spec
。
為此,我們建立一個 mock 例項作為我們的 mock 後端,併為其建立一個 mock 響應物件。為了將響應設定為最後一次 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)
有了這些,我們就可以將“模擬後端”打上補丁,並進行實際呼叫。
>>> 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
部分模擬¶
在某些測試中,我希望模擬對 datetime.date.today()
的呼叫以返回已知日期,但我不想阻止被測程式碼建立新的日期物件。不幸的是,datetime.date
是用 C 編寫的,因此我無法直接透過猴子補丁替換靜態的 datetime.date.today()
方法。
我發現了一種簡單的方法,它實際上是用 mock 包裝日期類,但將建構函式的呼叫傳遞給真實類(並返回真實例項)。
這裡使用 patch decorator
來模擬被測模組中的 date
類。然後將 mock date 類的 side_effect
屬性設定為返回真實日期的 lambda 函式。當呼叫 mock date 類時,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)
請注意,我們不是全域性修補 datetime.date
,而是修補 *使用* 它的模組中的 date
。請參閱在哪裡修補。
當呼叫 date.today()
時,會返回一個已知日期,但對 date(...)
建構函式的呼叫仍然返回正常日期。如果沒有這個,你可能會發現自己不得不使用與被測程式碼完全相同的演算法來計算預期結果,這是一種經典的測試反模式。
對 date 建構函式的呼叫記錄在 mock_date
屬性中(call_count
等),這對於你的測試也可能有用。
在這篇部落格文章中討論了處理模擬日期或其他內建類的另一種方法。
模擬生成器方法¶
Python 生成器是一個使用 yield
語句在迭代時返回一系列值的函式或方法[1]。
呼叫生成器方法/函式以返回生成器物件。然後迭代該生成器物件。迭代的協議方法是 __iter__()
,因此我們可以使用 MagicMock
來模擬它。
這是一個帶有作為生成器實現的“iter”方法的示例類。
>>> class Foo:
... def iter(self):
... for i in [1, 2, 3]:
... yield i
...
>>> foo = Foo()
>>> list(foo.iter())
[1, 2, 3]
我們如何模擬這個類,特別是它的“iter”方法?
要配置迭代返回的值(隱式地在對 list
的呼叫中),我們需要配置對 foo.iter()
呼叫返回的物件。
>>> mock_foo = MagicMock()
>>> mock_foo.iter.return_value = iter([1, 2, 3])
>>> list(mock_foo.iter())
[1, 2, 3]
將相同的補丁應用於每個測試方法¶
如果你想為多個測試方法設定多個補丁,最明顯的方法是將補丁裝飾器應用於每個方法。這會讓人覺得不必要的重複。相反,你可以將 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'
管理補丁的另一種方法是使用補丁方法:start 和 stop。這些方法允許你將補丁移動到你的 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
來確保補丁“撤銷”。這可能比你想象的更麻煩,因為如果在 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()
模擬非繫結方法¶
在編寫測試時,我需要修補一個 *非繫結方法*(修補類上的方法而不是例項上的方法)。我需要將 self 作為第一個引數傳入,因為我想斷言哪些物件正在呼叫這個特定方法。問題是不能用 mock 來修補,因為如果用 mock 替換一個非繫結方法,它在從例項中獲取時不會成為繫結方法,因此不會傳入 self。解決方法是用一個真實的函式來修補非繫結方法。patch()
裝飾器使得用 mock 修補方法變得如此簡單,以至於不得不建立一個真實的函式變得很麻煩。
如果你將 autospec=True
傳遞給 patch,它會用一個 *真實* 的函式物件進行修補。這個函式物件具有與它替換的函式相同的簽名,但在底層委託給一個 mock。你仍然可以像以前一樣自動建立 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 例項替換,並且不會以 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
為一。
>>> 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 'foo_bar' to be called once. Called 2 times.
Calls: [call('baz', spam='eggs'), call()].
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 remarkably 相似。
>>> 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 not found.
Expected: mock({1})
Actual: mock(set())
>>> c.foo
<CopyingMock name='mock.foo' id='...'>
當你子類化 Mock
或 MagicMock
時,所有動態建立的屬性和 return_value
都將自動使用你的子類。這意味著 CopyingMock
的所有子項也將具有 CopyingMock
型別。
巢狀補丁¶
使用 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 方法:start 和 stop,我們可以在沒有巢狀縮排的情況下實現相同的效果。一個簡單的輔助方法 create_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 模擬字典¶
你可能希望模擬字典或其他容器物件,記錄對其的所有訪問,同時讓它仍然像字典一樣執行。
我們可以使用 MagicMock
來實現這一點,它將像字典一樣執行,並使用 side_effect
將字典訪問委託給一個受我們控制的真實底層字典。
當我們的 MagicMock
的 __getitem__()
和 __setitem__()
方法被呼叫時(正常的字典訪問),side_effect
將與鍵(以及在 __setitem__
的情況下,也包括值)一起被呼叫。我們還可以控制返回的內容。
使用 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
有了這些副作用函式,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
屬性是 Mocks
,MagicMock
屬性是 MagicMocks
[2]。因此,如果你透過子類化來新增輔助方法,那麼它們也將在你的子類例項的屬性和返回值 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 介面卡。將此應用於屬性實際上會導致錯誤。
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
來影響匯入的結果。匯入會從 sys.modules
字典中獲取一個 *物件*。請注意,它獲取的是一個 *物件*,該物件不必是模組。第一次匯入模組會導致一個模組物件被放入 sys.modules
中,所以通常當你匯入某些東西時,你會得到一個模組。然而,情況並非總是如此。
這意味著你可以使用 patch.dict()
來 *暫時* 在 sys.modules
中放置一個 mock。在此補丁啟用期間的任何匯入都將獲取該 mock。當補丁完成時(裝飾函式退出,with 語句體完成或呼叫 patcher.stop()
),之前存在的任何內容都將安全恢復。
這是一個模擬“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()
>>> 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()]
然後,我們可以透過與管理器 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
相同的基本概念,我們可以實現匹配器,以對用作 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 not found.
Expected: mock(<__main__.Foo object at 0x...>)
Actual: mock(<__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) 的形式提供了類似的功能,這可能在此處有用。