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

備註

在大多數的範例中,MockMagicMock 類別是可以互換的。不過由於 MagicMock 是功能更強大的類別,因此通常它是一個更好的選擇。

一旦 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 開始,AsyncMockMagicMock 支援透過 __aiter__ 來 mock 异步迭代器__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 開始,AsyncMockMagicMock 支援透過 __aenter____aexit__ 來 mock 异步上下文管理器。預設情況下,__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

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)

另外,“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 移到你的 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 來 "取消" 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_withassert_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_argscall_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

另一種方法是建立 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: mock({1})
Actual call: mock(set())
>>> c.foo
<CopyingMock name='mock.foo' id='...'>

當你將 MockMagicMock 子類別化時,所有屬性會被動態建立,且 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

The standard behaviour for Mock 实例的标准行为是属性和返回值 mock 具有与它们所访问的 mock 相同的类型。 这将确保 Mock 的属性均为 MocksMagicMock 的属性均为 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 模拟导入

有一种会令模拟变困难的情况是当你在函数内部有局部导入。 这更难模拟的原因是它们不是使用来自我们能打补丁的模拟命名空间中的对象。

一般来说局部导入是应当避免的。 局部导入有时是为了防止循环依赖,而这个问题 通常 都有更好的解决办法(重构代码)或者通过延迟导入来防止“前期成本”。 这也可以通过比无条件地局部导入更好的方式来解决(将模块保存为一个类或模块属性并且只在首次使用时执行导入)。

除此之外还有一个办法可以使用 mock 来影响导入的结果。 导入操作会从 sys.modules 字典提取一个 对象。 请注意是提取一个 对象,它不是必须为模块。 首次导入一个模块将使一个模块对象被放入 sys.modules,因此通常当你执行导入时你将得到一个模块。 但是并非必然如此。

这意味着你可以使用 patch.dict()临时性地 将一个 mock 放入 sys.modules。 在补丁激活期间的任何导入操作都将得到该 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 的参数的对象上执行更复杂的断言。

假设我们准备将某个对象传给一个在默认情况下基于对象标识相等(这是 Python 中用户自定义类的默认行为)的 mock。 要使用 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)。