在Python中使用mock模組進行單元測試
為什麼需要Mock
假設現在系統有兩個模型A和B,其中A依賴B(例如A,B都是函式,A函式體內呼叫了B函式),但是B還沒完成,或者根本就不在控制之內;這時候又需要對A的功能進行單獨測試,就需要使用mock物件,模擬出一個假的fake_B模組,雖然這個fake_B模組是假的,但是我們可以通過對它的行為進行定製來使他能夠看起來“像”B模組的功能,使A依賴fake_B,來對A的功能進行測試。同時,由於fake_B是完全可控的,除了可以定製B的屬性,返回值之外,還可以對B模組的使用情況進行測試。
而在Python中可以使用mock和unittest.mock來產生這樣的mock物件。
用法簡介
首先需要建立一個mock物件
>>>from mock import MagicMock
>>>fake_obj = MagicMock()
然後可以通過return_value設定它的返回值,當你呼叫這個mock物件時返回,返回值可以是任何型別,變數,函式,類物件都可以。
>>>fake_obj.return_value = 'This is a mock object'
>>>fake_obj()
'This is a mock object'
也可以通過side_effect指定它的副作用,這個副作用就是當你呼叫這個mock物件是會呼叫的函式,也可以選擇丟擲一個異常,來對程式的錯誤狀態進行測試。
>>>def b():
... print 'This is b'
...
>>>fake_obj.side_effect = b
>>>fake_obj()
This is b
>>>fake_obj.side_effect = KeyError('This is b')
>>>fake_obj()
...
KeyError: 'This is b'
同時也可以以一個物件base為基礎,拓展mock,使得mock獲得base的所有屬性和方法(但僅是介面相同,沒有實現),當呼叫不屬於base的屬性和方法時,會丟擲AttributeError。這需要在建立mock物件的時候,在spec引數指定,前面的返回值和副作用也可以在建立時進行指定。
>>>class B:
... def __init__(self):
... self.a = 'a'
...
>>>b = B()
>>>fake_obj = MagicMock(b)
<MagicMock spec='B' id='4370614160'>
>>>fake_obj.a
'a'
>>>fake_obj.b
AttributeError: Mock object has no attribute 'b'
還有一點需要強調的就是,如果要模擬一個物件而不是函式,你可以直接在mock物件上新增屬性和方法,並且每一個新增的屬性都是一個mock物件,也就是說可以對這些屬性進行配置,並且可以一直遞迴的定義下去。
>>>fake_obj.fake_a.return_value = 'This is fake_obj.fake_a'
>>>fake_obj.fake_a()
'This is fake_obj.fake_a'
在對mock物件進行一定的配置之後就是使用mock物件進行測試,這個模組採用的‘action-assertion’的方式進行測試,即先執行,再通過斷言進行測試。例如可以測試mock物件是否被呼叫,呼叫了幾次,如何被呼叫等等,mock物件本身提供了豐富的斷言方法,官方文件中提供了詳盡的描述。
>>>fake_obj = MagicMock(return_value = 1)
>>>fake_obj.assert_called()
AssertionError: Expected 'None' to have been called.
>>>fake_obj()
>>>fake_obj.assert_called()
>>>
>>>fake_obj(1,2,3,key=1)
>>>fake_obj.assert_called_with(1,2,3,key=1)
>>>
同時mock庫提供了patch函式來簡化mock物件對原物件的替換。patch可以作為裝飾器或者上下文管理器使用,這意味著在裝飾的函式和上下文管理器中,對應的類會被替換為mock物件。
>>>from mock import patch
>>>@patch('__main__.SomeClass')
... def function(normal_argument, mock_class):
... print(mock_class is SomeClass)
...
>>>function(None)
True