自動化測試中,我們常會使用一些經(jīng)過簡化的,行為與表現(xiàn)類似于生產(chǎn)環(huán)境下的對象的復(fù)制品。引入這樣的復(fù)制品能夠降低構(gòu)建測試用例的復(fù)雜度,允許我們獨立而解耦地測試某個模塊,不再擔(dān)心受到系統(tǒng)中其他部分的影響。
在《The Art of Unit Testing》書中Mock 被描述為假對象,通過驗證是否發(fā)生與對象的交互來幫助確定測試是否失敗或通過。其他的東西都被定義為Stub。在這本書中,F(xiàn)ake對象就是不真實的,根據(jù)它們的使用情況,它們可以是Stub,也可以是Mock。
更復(fù)雜一點的定義是Gerard Meszaros 在XunitPatterns中對此類對象的定義。他對這類對象統(tǒng)一稱呼為:Test Double。包含:Dummy,F(xiàn)ake,Spy,Mock和Stub。

而通常,測試人員更傾向于使用 Mock 來統(tǒng)一描述不同的 Test Doubles。
不過對于 Test Doubles 實現(xiàn)的誤解還是可能會影響到測試的設(shè)計,使測試用例變得混亂和脆弱,最終帶來不必要的重構(gòu)。CC先生就最常用的Mock,F(xiàn)ake和Stub來解釋一下不同的 Double 的使用場景。
Fake:We use a Fake Object to replace the functionality of a real DOC in a test for reasons other than verification of indirect inputs and outputs of the SUT. Typically, it implements the same functionality as the real DOC but in a much simpler way. While a Fake Object is typically built specifically for testing, it is not used as either a control point or a observation point by the test.
簡單的來說,F(xiàn)ake 是那些包含了生產(chǎn)環(huán)境下具體實現(xiàn)的簡化版本的對象。
比如在測試系統(tǒng)時需要頻繁的連接數(shù)據(jù)庫進(jìn)行操作,而此時有可能數(shù)據(jù)庫還沒有完全實現(xiàn),我們就可以采用快速編寫系統(tǒng)原型,并且基于內(nèi)存存儲來運行整個系統(tǒng),推遲有關(guān)數(shù)據(jù)庫設(shè)計所用到的一些決定來加速測試環(huán)境的搭建。另一個常見的使用場景就是利用 Fake 來保證在測試環(huán)境下支付永遠(yuǎn)返回成功結(jié)果。
Stub:Test stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
Stub只是返回一個規(guī)定的值,而不會去涉及到系統(tǒng)的任何改變。
比較常見的場景就是系統(tǒng)希望去查詢某一類的信息,而Stub可以總是返回一個固定值,比如發(fā)送郵件的功能,Stub可以總是返回郵件發(fā)送成功的標(biāo)識1,但是你并不知道你到底發(fā)送了郵件給誰或者發(fā)送了幾封郵件。
Mock:We can use a Mock Object as an observation point that is used to verify the indirect outputs of the SUT as it is exercised. Typically, the Mock Object also includes the functionality of a Test Stub in that it must return values to the SUT if it hasn't already failed the tests but the emphasisis on the verification of the indirect outputs. Therefore, a Mock Object is lot more than just a Test Stub plus assertions; it is used a fundamentally different way.
就算在Gerard Meszaros的定義里面我們可以看出Mock和Stub有一定的重合性,比較大的區(qū)別是Mock專注于observation point,而Stub專注于control point,或者從另一個角度上面來說,Mock是會有行為的更改,而Stub只是狀態(tài)的一個變化而已。
在Python 3.3以前的版本中,需要另外安裝mock模塊,可以使用pip命令來安裝
pip install mock
使用的時候直接導(dǎo)入即可:
import mock
從Python 3.3開始,mock模塊已經(jīng)被合并到標(biāo)準(zhǔn)庫中,被命名為unittest.mock,可以直接import進(jìn)來使用:
from unittest import mock
也就是說我們以后使用Python的時候不用導(dǎo)入任何的第三方包就可以方便使用Mock來模擬測試對象的。Python中的Mock是非常容易使用,可以說是在unittest中使用最多。 模擬是基于“動作 - >斷言”模式,而不是許多Mock框架使用的“記錄 - >重放”。
Mock的基礎(chǔ)使用
Mock對象的一般用法是這樣的:
- 找到你要替換的對象,這個對象可以是一個類,或者是一個函數(shù),或者是一個類實例。
- 實例化Mock類得到一個mock對象,并且設(shè)置這個mock對象的行為,比如被調(diào)用的時候返回什么值,被訪問成員的時候返回什么值等。
- 使用這個mock對象替換掉我們想替換的對象,也就是步驟1中確定的對象。
之后就可以開始寫測試代碼,這個時候我們可以保證我們替換掉的對象在測試用例執(zhí)行的過程中行為和我們預(yù)設(shè)的一樣。
舉個例子: 簡單定義一個Person類,其中的代碼為:
class Person:
def __init__(self):
self.__age = 10
def get_fullname(self, first_name, last_name):
return first_name + ' ' + last_name
def get_age(self):
return self.__age
@staticmethod
def get_class_name():
return Person.__name__
類里有兩個成員方法,一個有參數(shù),一個無參數(shù),還有一個靜態(tài)方法
1). 使用Mock類,返回固定值
新建一個文件叫MockPerson.py,來測試:
from unittest import mock
import unittest
from .person import Person
class PersonTest(unittest.TestCase):
def test_should_get_age(self):
p = Person()
# 不mock時,get_age應(yīng)該返回10
self.assertEqual(p.get_age(), 10)
# mock掉get_age方法,讓它返回20
p.get_age = mock.Mock(return_value=20)
self.assertEqual(p.get_age(), 20)
def test_should_get_fullname(self):
p = Person()
# mock掉get_fullname,讓它返回'Tracy Cheng'
p.get_fullname = mock.Mock(return_value='Tracy cheng')
self.assertEqual(p.get_fullname(), 'Tracy cheng')
if __name__ == '__main__':
unittest.main()
返回固定值時,按照我們上面的名詞解釋,算是Stub的一種用法,只是用Mock類來實現(xiàn)的。
2). 使用side_effect,依次返回指定值:
class PersonTest(unittest.TestCase):
def test_should_get_age(self):
p = Person()
p.get_age = mock.Mock(side_effect=[10, 11, 12])
self.assertEqual(p.get_age(), 10)
self.assertEqual(p.get_age(), 11)
self.assertEqual(p.get_age(), 12)
get_page()每一次被調(diào)用的時候都會到Mock的side_effect中去取一個值。如果調(diào)用次數(shù)超過了side_effect中的個數(shù),程序運行時會報錯StopIteration。
3). 打算輸出為異常時:
p.get_age = mock.Mock(return_value =30,side_effect=Exception('Boom!'))
self.assertRaises(TypeError,p.get_age)
只要調(diào)用就會拋出異常。
- 檢驗是否調(diào)用
def test_should_validate_method_calling(self):
p = Person()
p.get_fullname = mock.Mock(return_value='Tracy cheng')
# 沒調(diào)用過
p.get_fullname.assert_not_called() # Python 3.5
p.get_fullname('1', '2')
# # 調(diào)用過任意次數(shù)
# p.get_fullname.assert_called() # Python 3.6
# # 只調(diào)用過一次, 不管參數(shù)
# p.get_fullname.assert_called_once() # Python 3.6
# 只調(diào)用過一次,并且符合指定的參數(shù)
p.get_fullname.assert_called_once_with('1', '2')
p.get_fullname('3', '4')
# 只要調(diào)用過即可,必須指定參數(shù)
p.get_fullname.assert_any_call('1', '2')
# 重置mock,重置之后相當(dāng)于沒有調(diào)用過
p.get_fullname.reset_mock()
p.get_fullname.assert_not_called()
# Mock對象里除了return_value, side_effect屬性外,
# called表示是否調(diào)用過,call_count可以返回調(diào)用的次數(shù)
self.assertEqual(p.get_fullname.called, False)
self.assertEqual(p.get_fullname.call_count, 0)
p.get_fullname('1', '2')
p.get_fullname('3', '4')
self.assertEqual(p.get_fullname.called, True)
self.assertEqual(p.get_fullname.call_count, 2)
其中的assert_called和assert_called_once是python3.6中的用法,注意一下Python的版本。
稍微高階一丟丟的用法:
靜態(tài)方法和模塊方法需要用到Patch來mock。其中會用到Patch裝修器,包含有: patch(), patch.object() and patch.dict().
patch和patch.object這兩個函數(shù)都會返回一個mock內(nèi)部的類實例,這個類是class _patch。返回的這個類實例既可以作為函數(shù)的裝飾器,也可以作為類的裝飾器,也可以作為上下文管理器。使用patch或者patch.object的目的是為了控制mock的范圍,意思就是在一個函數(shù)范圍內(nèi),或者一個類的范圍內(nèi),或者with語句的范圍內(nèi)mock掉一個對象。
# 在patch中給出定義好的Mock的對象,好處是定義好的對象可以復(fù)用
def test_should_get_class_name(self):
mock_get_class_name = mock.Mock(return_value='Man')
with mock.patch.object(Person,'get_class_name',mock_get_class_name):
self.assertEqual('Man',Person.get_class_name())
當(dāng)你知道了mock能做什么之后,要如何學(xué)習(xí)并掌握mock呢?最好的方式就是查看閱讀官方文檔,并在自己的單元測試中使用。
也有一些大神已經(jīng)封裝出更好使用的第三方Python Mock庫,可參見:
Python中好用的第三方mock庫-httmock
拓展:
Python3 mock模塊 -Python官網(wǎng)
Test Double - Martin Fowler
Test Double - xUnit Patterns
Mocks Aren't Stubs - Martin Fowler
Command Query Separation - Martin Fowler