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='...'>

一旦我們的模擬物件被使用(本例中為 real.method),它就具有允許你對它的使用方式進行斷言的方法和屬性。

注意

在大多數這些示例中,MockMagicMock 類是可以互換的。由於 MagicMock 是功能更強大的類,因此預設使用它是有意義的。

一旦模擬物件被呼叫,它的 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)

模擬物件上的方法呼叫

在最後一個示例中,我們直接在物件上修補了一個方法,以檢查它是否被正確呼叫。另一個常見的用例是將物件傳遞到方法(或被測系統的某些部分),然後檢查它是否以正確的方式使用。

下面簡單的 ProductionClass 有一個 closer 方法。如果使用物件呼叫它,則它會在該物件上呼叫 close

>>> class ProductionClass:
...     def closer(self, something):
...         something.close()
...

因此,要測試它,我們需要傳入一個具有 close 方法的物件,並檢查它是否被正確呼叫。

>>> real = ProductionClass()
>>> mock = Mock()
>>> real.closer(mock)
>>> mock.close.assert_called_with()

我們不必做任何工作來在我們的模擬物件上提供 “close” 方法。訪問 close 會建立它。因此,如果 “close” 尚未被呼叫,則在測試中訪問它將建立它,但是 assert_called_with() 將引發失敗異常。

模擬類

一個常見的用例是模擬被測程式碼例項化的類。當你修補一個類時,該類將替換為模擬物件。例項是透過呼叫該類建立的。這意味著你透過檢視模擬類的返回值來訪問“模擬例項”。

在下面的示例中,我們有一個函式 some_function,它例項化 Foo 並呼叫它的一個方法。對 patch() 的呼叫會將類 Foo 替換為模擬物件。 Foo 例項是呼叫模擬物件的結果,因此透過修改模擬物件的 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'

命名你的模擬物件

為你的模擬物件命名可能會很有用。該名稱會顯示在模擬物件的 repr 中,當模擬物件出現在測試失敗訊息中時可能會有所幫助。該名稱還會傳播到模擬物件的屬性或方法

>>> mock = MagicMock(name='foo')
>>> mock
<MagicMock name='foo' id='...'>
>>> mock.method
<MagicMock name='foo.method' id='...'>

跟蹤所有呼叫

通常,你想要跟蹤對方法的多次呼叫。 mock_calls 屬性記錄對模擬物件的子屬性的所有呼叫 - 以及對它們的子屬性的所有呼叫。

>>> 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

但是,不會記錄返回模擬物件的呼叫的引數,這意味著無法跟蹤建立祖先所用引數很重要的巢狀呼叫

>>> 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.return_value = 3
>>> mock()
3

當然,你可以對模擬物件的方法執行相同的操作

>>> mock = Mock()
>>> mock.method.return_value = 3
>>> mock.method()
3

返回值也可以在建構函式中設定

>>> mock = Mock(return_value=3)
>>> mock()
3

如果你需要在模擬物件上設定屬性,只需執行此操作即可

>>> 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() 的呼叫將我們的呼叫物件轉換為表示鏈式呼叫的呼叫列表。

使用模擬物件引發異常

一個有用的屬性是 side_effect。如果你將其設定為異常類或例項,則在呼叫模擬物件時將引發異常。

>>> mock = Mock(side_effect=Exception('Boom!'))
>>> mock()
Traceback (most recent call last):
  ...
Exception: Boom!

副作用函式和可迭代物件

side_effect 也可以設定為函式或可迭代物件。將 side_effect 用作可迭代物件的用例是你的模擬物件將被呼叫多次,並且你希望每次呼叫返回不同的值。當你將 side_effect 設定為可迭代物件時,每次呼叫模擬物件都會從可迭代物件返回下一個值

>>> mock = MagicMock(side_effect=[4, 5, 6])
>>> mock()
4
>>> mock()
5
>>> mock()
6

對於更高階的用例,例如根據模擬物件的呼叫方式動態更改返回值, side_effect 可以是一個函式。該函式將使用與模擬物件相同的引數進行呼叫。該函式返回的內容就是呼叫返回的內容

>>> 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 起,AsyncMockMagicMock 支援透過 __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 起,AsyncMockMagicMock 支援透過 __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

過度使用 mock 的一個問題是,它將你的測試與 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"

Patch 裝飾器

注意

使用 patch(),重要的是你要在查詢物件的名稱空間中修補它們。這通常很簡單,但要快速瞭解,請閱讀在哪裡修補

測試中的一個常見需求是修補類屬性或模組屬性,例如修補一個內建屬性或修補模組中的一個類以測試它是否被例項化。模組和類實際上是全域性的,因此在它們上進行修補後必須撤消,否則修補將持續到其他測試中,並導致難以診斷的問題。

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()(或有兩個引數的 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* 順序)傳遞給裝飾的函式。這意味著從下往上,所以在上面的示例中,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

patchpatch.objectpatch.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)

作為替代,patchpatch.objectpatch.dict 可以用作類裝飾器。以這種方式使用時,它與將裝飾器單獨應用於每個名稱以“test”開頭的方法相同。

更多示例

以下是一些針對稍微更高階場景的更多示例。

模擬鏈式呼叫

一旦你瞭解了 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 是否以正確的方式使用響應物件。

由於此呼叫鏈是從例項屬性發出的,我們可以在 Something 例項上 monkey-patch 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)

有了這些,我們可以在適當的位置 monkey-patch “mock 後端”,並可以進行實際呼叫

>>> 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 編寫的,因此我無法僅 monkey-patch 靜態 datetime.date.today() 方法。

我找到了一種簡單的方法來做到這一點,該方法有效地使用 mock 包裹 date 類,但將對建構函式的呼叫傳遞給實際類(並返回實際例項)。

這裡使用 patch 裝飾器 來模擬被測模組中的 date 類。然後,將模擬的 date 類的 side_effect 屬性設定為返回實際日期的 lambda 函式。當呼叫模擬的 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。這些允許您將補丁移動到您的 setUptearDown 方法中。

>>> 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 作為第一個引數傳入,因為我想斷言哪些物件正在呼叫此特定方法。問題是您不能使用模擬來修補此方法,因為如果您用模擬替換未繫結方法,則當從例項中獲取它時,它不會成為繫結方法,因此不會傳入 self。解決方法是用實際函式而不是模擬來修補未繫結方法。patch() 裝飾器使得用模擬修補方法變得非常簡單,以至於必須建立一個實際函式變成了一種麻煩。

如果您將 autospec=True 傳遞給 patch,那麼它將使用一個 *實際* 的函式物件進行修補。此函式物件具有與它要替換的函式物件相同的簽名,但在底層委託給模擬。您仍然以與以前完全相同的方式自動建立您的模擬。它的意思是,如果您使用它來修補類上的未繫結方法,則如果從例項中獲取,則模擬函式將轉換為繫結方法。它將以 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 有一個很好的 API,可以斷言您的模擬物件的使用方式。

>>> mock = Mock()
>>> mock.foo_bar.return_value = None
>>> mock.foo_bar('baz', spam='eggs')
>>> mock.foo_bar.assert_called_with('baz', spam='eggs')

如果您的模擬只被呼叫一次,您可以使用 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 'foo_bar' to be called once. Called 2 times.
Calls: [call('baz', spam='eggs'), call()].

assert_called_withassert_called_once_with 都對 *最近的* 呼叫進行斷言。如果您的模擬將被呼叫多次,並且您想對 *所有* 這些呼叫進行斷言,則可以使用 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

處理可變引數

另一種情況很少見,但會困擾您,那就是當您的模擬被可變引數呼叫時。call_argscall_args_list 儲存對引數的 *引用*。如果引數被被測程式碼修改,那麼您就無法再斷言呼叫模擬時的值是什麼。

這裡有一些示例程式碼顯示了這個問題。假設在“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(),), {})

一種可能性是讓模擬複製您傳入的引數。如果您進行依賴物件標識進行相等的斷言,這可能會導致問題。

這裡有一種使用 side_effect 功能的解決方案。如果您為模擬提供了一個 side_effect 函式,那麼將使用與模擬相同的引數呼叫 side_effect。這為我們提供了複製引數並將其儲存以供稍後斷言的機會。在此示例中,我正在使用 *另一個* 模擬來儲存引數,以便我可以使用模擬方法進行斷言。同樣,一個幫助函式為我設定了此操作。

>>> 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。它返回一個新的模擬,我們對它進行斷言。side_effect 函式建立引數的副本,並使用該副本呼叫我們的 new_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

另一種方法是建立 MockMagicMock 的子類,該子類複製(使用 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='...'>

當您對 MockMagicMock 進行子類化時,所有動態建立的屬性以及 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 可以設定 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__() 方法時(正常的字典訪問),會使用鍵(並且在 __setitem__ 的情況下還會使用值)呼叫 side_effect。我們還可以控制返回的內容。

在使用 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

注意

使用 Mock 的一個替代方法是使用 MagicMock,並提供您明確想要的魔法方法。

>>> 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)

使用 patch.dict 模擬匯入

難以模擬的一種情況是在函式內部有本地匯入。這些更難以模擬,因為它們沒有使用我們可以 patch 掉的模組名稱空間中的物件。

通常應避免本地匯入。有時這樣做是為了防止迴圈依賴,對於這種情況,通常有更好的方法來解決問題(重構程式碼)或透過延遲匯入來防止“前期成本”。也可以透過比無條件本地匯入更好的方式來解決此問題(將模組儲存為類或模組屬性,僅在首次使用時匯入)。

除此之外,有一種方法可以使用 mock 來影響匯入的結果。匯入從 sys.modules 字典中獲取物件。請注意,它獲取的是一個物件,該物件不必是模組。首次匯入模組會在 sys.modules 中放置一個模組物件,因此通常在匯入某些內容時會返回一個模組。但情況並非必須如此。

這意味著您可以使用 patch.dict()sys.modules臨時放置一個 mock。當此 patch 處於活動狀態時進行的任何匯入都將獲取該 mock。當 patch 完成時(裝飾的函式退出、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_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 的相等方法,該方法將模擬呼叫時傳遞的物件與我們建立匹配器的物件進行比較。如果它們匹配,則 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)。