使用 Python Mock 類進行單元測試


  數據類型、模型或節點——這些都只是mock對象可承擔的角色。但mock在單元測試中扮演一個什么角色呢?

有時,你需要為單元測試的初始設置准備一些“其他”的代碼資源。但這些資源興許會不可用,不穩定,或者是使用起來太笨重。你可以試着找一些其他的資源替代;或者你可以通過創建一個被稱為mock的東西來模擬它。Mocks能夠讓我們模擬那些在單元測試中不可用或太笨重的資源。

在Python中創建mock是通過Mock模塊完成的。你可以通過每次一個屬性(one-attribute-at-a-time)或一個健全的字典對象或是一個類接口來創建mock。你還可以定義mock的行為並且在測試過程中檢查它的使用。讓我們繼續探討。

 

測試准備

典型的測試准備最少有兩個部分。首先是測試對象(紅色),這是測試的關注點。它可以是一個方法、模塊或者類。它可以返回一個結果,也可以不返回結果,但是它可以根據數據數據或者內部狀態產生錯誤或者異常(圖1)。

Python

圖1

第二測試用例(灰色),它可以單獨運行也可以作為套件的一部分。它是為測試對象准備的,也可以是測試對象需要的任意數據或資源。運行一個或多個測試事務,在每個測試中檢查測試對象的行為。收集測試結果並用一個簡潔、易讀的格式呈現測試結果。

現在,為了發揮作用,一些測試對象需要一個或多個資源(綠色)。這些資源可以是其他的類或者模塊,甚至是一個非獨立的進程。不論其性質,測試資源是功能性的代碼。他們的角色是支持測試對象,但是他們不是測試的關注點。

使用Mock的理由

但是有些時候,測試資源不可用,或者不適合。也許這個資源正在和測試對象並行開發中,或者並不完整或者是太不穩定以至於不可靠。

測試資源太昂貴,如果測試資源是第三方的產品,其高昂的價格不適用於測試。測試資源的建立過於復雜,占用的硬件和時間可以用於別的地方。如果測試資源是一個數據源,建立它的數據集模仿真實世界是乏味的。

測試資源是不可預知的。一個好的單元測試是可重復的,允許你分離和識別故障。但是測試資源可能給出隨機的結果,或者它會有不同的響應時間。而作為這樣的結果,測試資源最終可能成為一個潛在的攪局者。

 這些都是你可能想要用mock代替測試資源的原因。mock向測試對象提供一套和測試資源相同的方法接口。但是mock是更容易創建和管理。它能向測試對象提供和真實的測試資源相同的方法接口。它能提供確定的結果,並可以自定義以適用於特定的測試。能夠容易的更新,以反映實際資源的變化。

當然,mocks不是沒有問題的。設計一個精確的mock是困難的,特別是如果你沒有測試資源的可靠信息。你可以嘗試找到一個開源的接口,或者你能對測試資源的方法接口進行猜測。無論你如何選擇,你都可以在以后輕松的更新mock,你可以在首選資源中得到更詳細的信息。

太多的mock會使測試過於復雜,讓你跟蹤錯誤變得更困難。最好的實踐是每個測試用例限制使用一到兩個mock,或者為每個mock/對象對使用獨立的測試用例。

Mocks對Stubs對Fakes

Mock不是模仿測試資源的唯一方式。其他的解決方案如stub和fake也能提供相同的服務。因此,mock和其他兩種解決方案怎樣比較?為什么選擇mock而不是選擇stub或者fake?

認識stub:stub為測試對象提供了一套方法接口,和真實的測試資源提供給測試對象的接口是相同的。當測試對象調用stub方法時,stub響應預定的結果。也可以產生一個預定的錯誤或者異常。stub可以跟蹤和測試對象的交互,但是它不處理輸入的數據。

fake也提供了一套方法接口並且也可以跟蹤和測試對象的交互。但是和stub不同,fake真正的處理了從測試對象輸入的數據產生的結果是基於這些數據的。簡而言之,fake是功能性的,它是真實測試資源的非生產版。它缺乏資源的相互制衡,使用了更簡單的算法,而且它很少存儲和傳輸數據。

使用fake和stub,你可以輸入正確的數據調用了正確的方法對測試對象進行測試。你能測試對象是如何處理數據並產生結果,當出現錯誤或者異常時是怎樣反應的。這些測試被稱為狀態驗證。但是你是否想知道測試對象調用了兩次相同的方法?你是否想知道測試對象是否按照正確的順序調用了幾個方法?這種測試被稱為行為驗證,而要做到這些,你需要mocks。

使用Python Mock

在Python中Mock模塊是用來創建和管理mock對象的。該模塊是Michael Foord的心血結晶,它是Python3.0的標准模塊。因此在Python2.4~2.7中,你不得不自己安裝這個模塊。你可以 Python Package Index website從獲得Mock模塊最新的版本。

基於你的mock對象,Mock模塊提供了少量的類。為了改變運行中的mock甚至提供了補丁機制。但是現在,我們關注一個類:Mock類。

圖2中顯示了Mock類(綠色)的基本結構。它繼承於兩個父類:NonCallableMock和CallableMixin(灰色)。NonCallableMock定義了mock對象所需的例程。它重載了幾個魔法方法,給他們定義了缺省的行為。為了跟蹤mock的行為,它提供了斷言例程。CallableMixin更新了mock對象回調的魔法方法。反過來,兩個父類繼承於Base類(紅色),聲明了mock對象所需的屬性。

Python

圖2

准備Mock

 

Mock類有四套方法(圖3)。第一套方法是類的構造器,它有六個可選和已標記的參數。圖中顯示了4個經常用到的參數。

Python

圖3

構造器的第一個參數是name,它定義了mock對象的唯一標示符。Listing one顯示了怎么創建一個標示符為Foo的mock對象mockFoo。請注意當我打印mock對象(6-9行)時,標示符后緊跟的是mock對象的唯一ID。

構造器的第一個參數是name,它定義了mock對象的唯一標示符。Listing one顯示了怎么創建一個標示符為Foo的mock對象mockFoo。請注意當我打印mock對象(6-9行)時,標示符后緊跟的是mock對象的唯一ID。

Listing One

1 from mock import Mock
2 
3 #create the mock object
4 mockFoo = Mock(name = "Foo")
5 
6 print mockFoo
7 
8 print repr(mockFoo)

構造器的第二個參數是spec。它設置mock對象的屬性,可以是property或者方法。屬性可以是一個列表字符串或者是其他的Python類。

為了演示,在Listing Two中,我有一個帶三個項目的列表對象fooSpec(第4行):property屬性_fooValue,方法屬性callFoo和doFoo。當我把fooSpec帶入類構造器時(第7行),mockFoo獲得了三個屬性,我能用點操作符訪問它們(10~15行)。當我訪問了一個未聲明的屬性時,mockFoo引發AttributeError和"faulty"屬性(21~14行)。

Listing Two

 1 from mock import Mock
 2  
 3 # prepare the spec list
 4 fooSpec = ["_fooValue", "callFoo", "doFoo"]
 5  
 6 # create the mock object
 7 mockFoo = Mock(spec = fooSpec)
 8  
 9 # accessing the mocked attributes
10 print mockFoo
11 # <Mock id='427280'>
12 print mockFoo._fooValue
13 # returns <Mock name='mock._fooValue' id='2788112'>
14 print mockFoo.callFoo()
15 # returns: <Mock name='mock.callFoo()' id='2815376'>
16  
17 mockFoo.callFoo()
18 # nothing happens, which is fine
19  
20 # accessing the missing attributes
21 print mockFoo._fooBar
22 # raises: AttributeError: Mock object has no attribute '_fooBar'
23 mockFoo.callFoobar()
24 # raises: AttributeError: Mock object has no attribute 'callFoobar'

Listing Three顯示了spec參數的另一種用法。這次,有帶三個屬性的類Foo(4~12行)。把類名傳入構造器中,這樣就產生了一個和Foo有相同屬性的mock對象(18~23行)。再次,訪問一個未聲明的屬性引發了AttributeError(29~32行)。也就是說,在兩個例子中,方法屬性時沒有功能的。甚至在方法有功能代碼時,調用mock的方法也什么都不做。

Listing Three

 1 from mock import Mock
 2  
 3 # The class interfaces
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         print "Foo:callFoo_"
10      
11     def doFoo(self, argValue):
12         print "Foo:doFoo:input = ", argValue    
13  
14 # create the mock object
15 mockFoo = Mock(spec = Foo)
16  
17 # accessing the mocked attributes
18 print mockFoo
19 # returns <Mock spec='Foo' id='507120'>
20 print mockFoo._fooValue
21 # returns <Mock name='mock._fooValue' id='2788112'>
22 print mockFoo.callFoo()
23 # returns: <Mock name='mock.callFoo()' id='2815376'>
24  
25 mockFoo.callFoo()
26 # nothing happens, which is fine
27  
28 # accessing the missing attributes
29 print mockFoo._fooBar
30 # raises: AttributeError: Mock object has no attribute '_fooBar'
31 mockFoo.callFoobar()
32 # raises: AttributeError: Mock object has no attribute 'callFoobar'

 下一個構造器參數是return_value。這將設置mock對象的響應當它被直接調用的時候。我用這個參數模擬一個工廠調用。 

為了演示,在Listing Four中,設置return_value為456(第4行)。當調用mockFoo時,將返回456的結果(9~11行)。在Listing Five中,我給return_value傳入了一個類Foo的實例fooObj(15~19行)。現在,當我調用mockFoo時,我獲得了fooObj的實例(顯示為mockObj)(第24行)。和Listing Two和Three不一樣,mockObj的方法是帶有功能的。

Listing Four

 1 from mock import Mock
 2  
 3 # create the mock object
 4 mockFoo = Mock(return_value = 456)
 5  
 6 print mockFoo
 7 # <Mock id='2787568'>
 8  
 9 mockObj = mockFoo()
10 print mockObj
11 # returns: 456

Listing Five

 1 from mock import Mock
 2  
 3 # The mock object
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         print "Foo:callFoo_"
10      
11     def doFoo(self, argValue):
12         print "Foo:doFoo:input = ", argValue
13  
14 # creating the mock object
15 fooObj = Foo()
16 print fooObj
17 # returns: <__main__.Foo object at 0x68550>
18  
19 mockFoo = Mock(return_value = fooObj)
20 print mockFoo
21 # returns: <Mock id='2788144'>
22  
23 # creating an "instance"
24 mockObj = mockFoo()
25 print mockObj
26 # returns: <__main__.Foo object at 0x68550>
27  
28 # working with the mocked instance
29 print mockObj._fooValue
30 # returns: 123
31 mockObj.callFoo()
32 # returns: Foo:callFoo_
33 mockObj.doFoo("narf")
34 # returns: Foo:doFoo:input =  narf
35 <Mock id='428560'>

side_effect參數和return_value是相反的。它給mock分配了可替換的結果,覆蓋了return_value。簡單的說,一個模擬工廠調用將返回side_effect值,而不是return_value。 

Listing Six演示了side_effect參數的影響。首先,創建類Foo的實例fooObj,把它傳入return_value參數(第17行)。這個結果和Listing Five是類似的。當它被調用的時候,mockFoo返回fooObj(第22行)。然后我重復同樣的步驟,給side_effect參數傳入StandardError(第28行),現在,調用mockFoo引發了StandardError,不再返回fooObj(29~30行)。

Listing Six

 1 from mock import Mock
 2  
 3 # The mock object
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         print "Foo:callFoo_"
10      
11     def doFoo(self, argValue):
12         print "Foo:doFoo:input = ", argValue
13  
14 # creating the mock object (without a side effect)
15 fooObj = Foo()
16  
17 mockFoo = Mock(return_value = fooObj)
18 print mockFoo
19 # returns: <Mock id='2788144'>
20  
21 # creating an "instance"
22 mockObj = mockFoo()
23 print mockObj
24 # returns: <__main__.Foo object at 0x2a88f0>
25  
26 # creating a mock object (with a side effect)
27  
28 mockFoo = Mock(return_value = fooObj, side_effect = StandardError)
29 mockObj = mockFoo()
30 # raises: StandardError

Listing Seven顯示了另一個影響。在這個例子中,傳入一個列表對象fooList到類構造器中(17~18行)。然后,每次我調用mockFoo時,它連續的返回列表中的項(20~30行)。一旦mockFoo到達了列表的末尾,調用將引發StopIteration 錯誤(32~34行)

Listing Seven

 1 from mock import Mock
 2  
 3 # The mock object
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         print "Foo:callFoo_"
10      
11     def doFoo(self, argValue):
12         print "Foo:doFoo:input = ", argValue
13  
14 # creating the mock object (with a side effect)
15 fooObj = FooSpec()
16  
17 fooList = [665, 666, 667]
18 mockFoo = Mock(return_value = fooObj, side_effect = fooList)
19  
20 fooTest = mockFoo()
21 print fooTest
22 # returns 665
23  
24 fooTest = mockFoo()
25 print fooTest
26 # returns 666
27  
28 fooTest = mockFoo()
29 print fooTest
30 # returns 667
31  
32 fooTest = mockFoo()
33 print fooTest
34 # raises: StopIteration

你可以傳入其他的可迭代對象(集合,元組)到side_effct對象中。你不能傳入一個簡單對象(如整數、字符串等),因為這些對象是不能迭代的,為了讓簡單對象可迭代,需要將他們加入單一項的列表中。

Mock斷言

Mock類的下一套方法是斷言。它將幫助跟蹤測試對象對mock方法的調用。他們能和unittest模塊的斷言一起工作。能連接到mock或者其方法屬性之一。 有兩個相同的可選參數:一個變量序列,一個鍵/值序列。

第一個斷言assert_called_with(),檢查mock方法是否獲得了正確的參數。當至少一個參數有錯誤的值或者類型時,當參數的數量錯誤時,當參數的順序錯誤時,或者當mock的方法根本不存在任何參數時,這個斷言將引發錯誤。Listing Eight顯示了可以怎樣使用這個斷言。那兒,我准備了一個mock對象,用類Foo作為它的spec參數。我調用了類的方法doFoo(),傳入了一個字符串作為輸入。使用assert_called_with(),我檢查方法是否獲得了正確的輸入。第20行的斷言通過了,因為doFoo()獲得了"narf"的輸入。但是在第24行的斷言失敗了因為doFoo()獲得了"zort",這是錯誤的輸入。

Listing Eight

 1 from mock import Mock
 2  
 3 # The mock object
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         pass
10      
11     def doFoo(self, argValue):
12         pass
13  
14 # create the mock object
15 mockFoo = Mock(spec = Foo)
16 print mockFoo
17 # returns <Mock spec='Foo' id='507120'>
18  
19 mockFoo.doFoo("narf")
20 mockFoo.doFoo.assert_called_with("narf")
21 # assertion passes
22  
23 mockFoo.doFoo("zort")
24 mockFoo.doFoo.assert_called_with("narf")
25 # AssertionError: Expected call: doFoo('narf')
26 # Actual call: doFoo('zort')

Listing Nine顯示了稍微不同的用法。在這個例子中,我調用了mock方法callFoo(),首先沒有任何輸入,然后輸入了字符串“zort”。第一個斷言通過了(第20行),因為callFoo()不支持獲得任何輸入。而第二個斷言失敗了(第24行)因為顯而易見的原因。

Listing Nine

 1 from mock import Mock
 2  
 3 # The mock object
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         pass
10      
11     def doFoo(self, argValue):
12         pass
13  
14 # create the mock object
15 mockFoo = Mock(spec = Foo)
16 print mockFoo
17 # returns <Mock spec='Foo' id='507120'>
18  
19 mockFoo.callFoo()
20 mockFoo.callFoo.assert_called_with()
21 # assertion passes
22  
23 mockFoo.callFoo("zort")
24 mockFoo.callFoo.assert_called_with()
25 # AssertionError: Expected call: callFoo()
26 # Actual call: callFoo('zort')

先一個斷言是assert_called_once_with()。像assert_called_with()一樣,這個斷言檢查測試對象是否正確的調用了mock方法。但是當同樣的方法調用超過一次時, assert_called_once_with()將引發錯誤,然而assert_called_with()會忽略多次調用。Listing Ten顯示了怎樣使用這個斷言。那兒,我調用了mock方法callFoo()兩次。第一次調用時(行19~20),斷言通過。但是在第二次調用的時(行23~24),斷言失敗,發送了錯誤消息到stdout。

Listing Ten

 1 from mock import Mock
 2  
 3 # The mock object
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         pass
10      
11     def doFoo(self, argValue):
12         pass
13  
14 # create the mock object
15 mockFoo = Mock(spec = Foo)
16 print mockFoo
17 # returns <Mock spec='Foo' id='507120'>
18  
19 mockFoo.callFoo()
20 mockFoo.callFoo.assert_called_once_with()
21 # assertion passes
22  
23 mockFoo.callFoo()
24 mockFoo.callFoo.assert_called_once_with()
25 # AssertionError: Expected to be called once. Called 2 times.

斷言assert_any_call(),檢查測試對象在測試例程中是否調用了測試方法。它不管mock方法和斷言之間有多少其他的調用。和前面兩個斷言相比較,前兩個斷言僅檢查最近一次的調用。

Listing Eleven顯示了assert_any_call()斷言如何工作:仍然是同樣的mock對象,spec參數是Foo類。第一個調用方法callFoo()(第18行),接下來調用兩次doFoo()(行19~20)。注意doFoo()獲得了兩個不同的輸入。

Listing Eleven

 1 <from mock import Mock
 2  
 3 # The mock specification
 4 class Foo(object):
 5     _fooValue = 123
 6      
 7     def callFoo(self):
 8         pass
 9      
10     def doFoo(self, argValue):
11         pass
12  
13 # create the mock object
14 mockFoo = Mock(spec = Foo)
15 print mockFoo
16 # returns <Mock spec='Foo' id='507120'>
17  
18 mockFoo.callFoo()
19 mockFoo.doFoo("narf")
20 mockFoo.doFoo("zort")
21  
22 mockFoo.callFoo.assert_any_call()
23 # assert passes
24  
25 mockFoo.callFoo()
26 mockFoo.doFoo("troz")
27  
28 mockFoo.doFoo.assert_any_call("zort")
29 # assert passes
30  
31 mockFoo.doFoo.assert_any_call("egad")
32 # raises: AssertionError: doFoo('egad') call not found

第一個assert_any_call()(第22行)通過,雖然兩次doFoo()調用隔開了斷言和callFoo()。第二個斷言(第28行)也通過了,雖然一個callFoo()隔開了我們提到的doFoo()(第20行)。另一方面,第三個斷言(第31行)失敗了,因為沒有任何doFoo()的調用使用了"egad"的輸入。

最后,還有assert_has_calls()。它查看方法調用的順序,檢查他們是否按正確的次序調用並帶有正確的參數。它帶有兩個參數:期望調用方法的列表和一個可選懸殊any_order。當測試對象調用了錯誤的方法,調用了不在次序中的方法,或者方法獲得了一個錯誤的輸入,將生產斷言錯誤。

Listing Twelve演示了assert_has_calls()斷言。在18~20行,我調用了三個方法,提供了兩個輸入。然后,我准備了一個期望調用的列表(fooCalls)並把這個列表傳入assert_has_calls()(22~23行)。由於列表匹配了方法的調用,斷言通過。

Listing Twelve

 1 from mock import Mock, call
 2  
 3 # The mock specification
 4 class Foo(object):
 5     _fooValue = 123
 6      
 7     def callFoo(self):
 8         pass
 9      
10     def doFoo(self, argValue):
11         pass
12  
13 # create the mock object
14 mockFoo = Mock(spec = Foo)
15 print mockFoo
16 # returns <Mock spec='Foo' id='507120'>
17  
18 mockFoo.callFoo()
19 mockFoo.doFoo("narf")
20 mockFoo.doFoo("zort")
21  
22 fooCalls = [call.callFoo(), call.doFoo("narf"), call.doFoo("zort")]
23 mockFoo.assert_has_calls(fooCalls)
24 # assert passes
25  
26 fooCalls = [call.callFoo(), call.doFoo("zort"), call.doFoo("narf")]
27 mockFoo.assert_has_calls(fooCalls)
28 # AssertionError: Calls not found.
29 # Expected: [call.callFoo(), call.doFoo('zort'), call.doFoo('narf')]
30 # Actual: [call.callFoo(), call.doFoo('narf'), call.doFoo('zort')]
31  
32 fooCalls = [call.callFoo(), call.doFoo("zort"), call.doFoo("narf")]
33 mockFoo.assert_has_calls(fooCalls, any_order = True)

在第26行,我交換了兩個doFoo()調用的順序。第一個doFoo()獲得"zort"的輸入,第二個獲得了"narf"。如果我傳入這個fooCalls到assert_has_calls()(第27行)中,斷言失敗。但是如果我給參數any_order傳入參數True,斷言通過。這是因為斷言將忽略方法調用的順序。

 

Listing Thirteen演示了其他的用法。在fooCalls列表中,我添加了不存在的方法dooFoo()(第22行)。然后我傳入fooCalls到assert_has_calls()中(第24行)。斷言失敗,通知我期望調用的順序和真實發生的順序不匹配。如果我給any_order賦值為True(第30行),斷言名稱dooFoo()作為違規的方法調用。

Listing Thirteen

 1 from mock import Mock, call
 2  
 3 # The mock specification
 4 class Foo(object):
 5     _fooValue = 123
 6      
 7     def callFoo(self):
 8         pass
 9      
10     def doFoo(self, argValue):
11         pass
12  
13 # create the mock object
14 mockFoo = Mock(spec = Foo)
15 print mockFoo
16 # returns <Mock spec='Foo' id='507120'>
17  
18 mockFoo.callFoo()
19 mockFoo.doFoo("narf")
20 mockFoo.doFoo("zort")
21  
22 fooCalls = [call.callFoo(), call.dooFoo("narf"), call.doFoo("zort")]
23  
24 mockFoo.assert_has_calls(fooCalls)
25 # AssertionError: Calls not found.
26 # Expected: [call.callFoo(), call.dooFoo('narf'), call.doFoo('zort')]
27 # Actual: [call.callFoo(), call.doFoo('narf'), call.doFoo('zort')]
28  
29 fooCalls = [call.callFoo(), call.dooFoo("narf"), call.doFoo("zort")]
30 mockFoo.assert_has_calls(fooCalls, any_order = True)
31 # AssertionError: (call.dooFoo('narf'),) not all found in call list

在assert_has_calls()的兩個例子中,注意到關鍵字call是出現在每個方法的前面。這個關鍵字是一個helper對象,標記出mock對象的方法屬性。為了使用call關鍵字,請確保使用如下的方法從mocke模塊導入helper:

1 from mock import Mock, call

管理Mock

Mock類的第三套方法允許你控制和管理mock對象。你可以更改mock的行為,改變它的屬性或者將mock恢復到測試前的狀態。你甚至可以更改每個mock方法或者mock本身的響應值。attach_mock()方法讓你在mock中添加第二個mock對象。這個方法帶有兩個參數:第二個mock對象(aMock)和一個屬性名稱(aName)。

Listing Fourteen 樣式了attach_mock()方法的使用。那兒,我創建了兩個mock對象mockFoo和mockBar,他們有不同spec參數(第25行和第30行)。我用attach_mock()方法將mockBar添加到mockFoo中,命名為fooBar(第35行)。一旦添加成功,我就能通過property fooBar訪問第二mock對象和它的屬性(46~53行)。並且我仍然可以訪問第一個mock對象mockFoo的屬性。

Listing Fourteen

 1 from mock import Mock
 2  
 3 # The mock object
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         print "Foo:callFoo_"
10      
11     def doFoo(self, argValue):
12         print "Foo:doFoo:input = ", argValue
13  
14 class Bar(object):
15     # instance properties
16     _barValue = 456
17      
18     def callBar(self):
19         pass
20      
21     def doBar(self, argValue):
22         pass
23  
24 # create the first mock object
25 mockFoo = Mock(spec = Foo)
26 print mockFoo
27 # returns <Mock spec='Foo' id='507120'>
28  
29 # create the second mock object
30 mockBar = Mock(spec = Bar)
31 print mockBar
32 # returns: <Mock spec='Bar' id='2784400'>
33  
34 # attach the second mock to the first
35 mockFoo.attach_mock(mockBar, 'fooBar')
36  
37 # access the first mock's attributes
38 print mockFoo
39 # returns: <Mock spec='Foo' id='495312'>
40 print mockFoo._fooValue
41 # returns: <Mock name='mock._fooValue' id='428976'>
42 print mockFoo.callFoo()
43 # returns: <Mock name='mock.callFoo()' id='448144'>
44  
45 # access the second mock and its attributes
46 print mockFoo.fooBar
47 # returns: <Mock name='mock.fooBar' spec='Bar' id='2788592'>
48 print mockFoo.fooBar._barValue
49 # returns: <Mock name='mock.fooBar._barValue' id='2788016'>
50 print mockFoo.fooBar.callBar()
51 # returns: <Mock name='mock.fooBar.callBar()' id='2819344'>
52 print mockFoo.fooBar.doBar("narf")
53 # returns: <Mock name='mock.fooBar.doBar()' id='4544528'>

configure_mock()方法讓你批量的更改mock對象。它唯一的參數是一個鍵值對序列,每個鍵就是你想要修改的屬性。如果你的對象沒有指定的屬性,configure_mock()將在mock中添加屬性。 

 

Listing fifteen顯示了configure_mock()方法的運用。再次,我定義了一個spec為類Foo和return_value為555的mock對象mockFoo(第13行)。然后使用configure_mock()方法更改return_value為999(第17行)。當我直接調用mockFoo時,獲得的結果為999,替換了原來的555。

Listing Fifteen

 1 from mock import Mock
 2  
 3 class Foo(object):
 4     # instance properties
 5     _fooValue = 123
 6      
 7     def callFoo(self):
 8         print "Foo:callFoo_"
 9      
10     def doFoo(self, argValue):
11         print "Foo:doFoo:input = ", argValue
12  
13 mockFoo = Mock(spec = Foo, return_value = 555)
14 print mockFoo()
15 # returns: 555
16  
17 mockFoo.configure_mock(return_value = 999)
18 print mockFoo()
19 # returns: 999
20  
21 fooSpec = {'callFoo.return_value':"narf", 'doFoo.return_value':"zort", 'doFoo.side_effect':StandardError}
22 mockFoo.configure_mock(**fooSpec)
23  
24 print mockFoo.callFoo()
25 # returns: narf
26 print mockFoo.doFoo("narf")
27 # raises: StandardError
28  
29 fooSpec = {'doFoo.side_effect':None}
30 mockFoo.configure_mock(**fooSpec)
31 print mockFoo.doFoo("narf")
32 # returns: zort

接着,我准備了一個字段對象(fooSpec),對兩個mock方法設置了返回值,為doFoo()設置了side_effect(第21行)。我將fooSpec傳入configure_mock(),注意fooSpec帶有前綴'**'(第22行)。現在調用callFoo()結果返回“narf”。調用doFoo(),無論輸入什么,引發StandardError 信號(行24~27)。如果我修改了fooSpec,設置doFoo()的side_effect的值為None,當我調用doFoo()時,將得到結果“zort”(29~32行)。

 

下一個方法mock_add_spec()讓你向mock對象添加新的屬性。除了mock_add_spec()工作在一個已存在的對象上之外,它的功能類似於構造器的spec參數。它擦除了一些構造器設置的屬性。這個方法帶有兩個參數:spec屬性(aSpec)和spc_set標志(aFlag)。再次,spce可以是字符串列表或者是類。已添加的屬性缺省狀態是只讀的,但是通過設置spec_set標志為True,可以讓屬性可寫。

Listing Sixteen演示了mock_add_spec()的運用。mock對象mockFoo開始的屬性來自於類Foo(第25行)。當我訪問兩個屬性(_fooValue和callFoo())時,我得到結果確認他們是存在的(29~32行)。

Listing Sixteen

 1 from mock import Mock
 2  
 3 # The class interfaces
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         print "Foo:callFoo_"
10      
11     def doFoo(self, argValue):
12         print "Foo:doFoo:input = ", argValue
13  
14 class Bar(object):
15     # instance properties
16     _barValue = 456
17      
18     def callBar(self):
19         pass
20      
21     def doBar(self, argValue):
22         pass
23      
24 # create the mock object
25 mockFoo = Mock(spec = Foo)
26  
27 print mockFoo
28 # returns <Mock spec='Foo' id='507120'>
29 print mockFoo._fooValue
30 # returns <Mock name='mock._fooValue' id='2788112'>
31 print mockFoo.callFoo()
32 # returns: <Mock name='mock.callFoo()' id='2815376'>
33  
34 # add a new spec attributes
35 mockFoo.mock_add_spec(Bar)
36  
37 print mockFoo
38 # returns: <Mock spec='Bar' id='491088'>
39 print mockFoo._barValue
40 # returns: <Mock name='mock._barValue' id='2815120'>
41 print mockFoo.callBar()
42 # returns: <Mock name='mock.callBar()' id='4544368'>
43  
44 print mockFoo._fooValue
45 # raises: AttributeError: Mock object has no attribute '_fooValue'
46 print mockFoo.callFoo()
47 # raises: AttributeError: Mock object has no attribute 'callFoo'

然后,我使用mock_add_spec()方法添加類Bar到mockFoo(第35行)。mock對象現在的屬性已聲明在類Bar中(39~42行)。如果我訪問任何Foo屬性,mock對象將引發AttributeError 信號,表示他們不存在(44~47行)。

 

最后一個方法resetMock(),恢復mock對象到測試前的狀態。它清除了mock對象的調用統計和斷言。它不會清除mock對象的return_value和side_effect屬性和它的方法屬性。這樣做是為了重新使用mock對象避免重新創建mock的開銷。

最后,你能給每個方法屬性分配返回值或者side-effect。你能通過return_value和side_effect訪問器做到這些。例如,按如下的語句通過return_value訪問器設置方法callFoo()的返回值為"narf":

1 mockFoo.callFoo.return_value = "narf"

按如下的語句通過side_effect訪問器 設置方法callFoo()的side-ffect為TypeError

1 mockFoo.callFoo.side_effect = TypeError

傳入None清除side-effect

1 mockFoo.callFoo.side_effect = None

你也可以用這個兩個相同的訪問器改變mock對象對工廠調用的響應值。 

 

Mock統計

最后一套方法包含跟蹤mock對象所做的任意調用的訪問器。當mock對象獲得工廠調用時,訪問器called返回True,否則返回False。查看Listing Seventeen中的代碼,我創建了mockFoo之后,called訪問器返回了結果False(19~20行)。如果我做了一個工廠調用,它將返回結果True(22~23行)。但是如果我創建了第二個mock對象,然后調用了mock方法callFoo()(第30行)?在這個例子中,called訪問器僅僅放回了False結果(31~32行)。

Listing Seventeen

 1 from mock import Mock
 2  
 3 # The mock object
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         print "Foo:callFoo_"
10      
11     def doFoo(self, argValue):
12         print "Foo:doFoo:input = ", argValue
13  
14 # create the first mock object
15 mockFoo = Mock(spec = Foo)
16 print mockFoo
17 # returns <Mock spec='Foo' id='507120'>
18  
19 print mockFoo.called
20 # returns: False
21  
22 mockFoo()
23 print mockFoo.called
24 # returns: True
25  
26 mockFoo = Mock(spec = Foo)
27 print mockFoo.called
28 # returns: False
29  
30 mockFoo.callFoo()
31 print mockFoo.called
32 # returns: False

訪問器call_count給出了mock對象被工廠調用的次數。查看Listing Eighteen中的代碼。我創建mockFoo之后,call_count給出的期望結果為0(19~20行)。當我對mockFoo做了一個工廠調用時,call_count增加1(22~24行)。當我調用mock方法callFoo()時,call_count沒有改變(26~28行)。如果我做了第二次工廠調用call_count將再增加1。

Listing Eighteen

 1 from mock import Mock
 2  
 3 # The mock object
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         print "Foo:callFoo_"
10      
11     def doFoo(self, argValue):
12         print "Foo:doFoo:input = ", argValue
13  
14 # create the first mock object
15 mockFoo = Mock(spec = Foo)
16 print mockFoo
17 # returns <Mock spec='Foo' id='507120'>
18  
19 print mockFoo.call_count
20 # returns: 0
21  
22 mockFoo()
23 print mockFoo.call_count
24 # returns: 1
25  
26 mockFoo.callFoo()
27 print mockFoo.call_count
28 # returns: 1

訪問器call_args返回工廠調用已用的參數。Listing Nineteen演示了它的運用。對於新創建的mock對象(mockFoo),call_args訪問器返回結果為None(17~21行)。如果我做了一個工廠調用,在輸入中傳入"zort",call_args報告的結果為call('zort')(23~25行)。注意結果中的call關鍵字。對於第二個沒有輸入的工廠調用,call_args返回call()(27~29行)。第三個工廠調用,輸入“troz”,call_args給出結果為call('troz')(31~33行)。但是當我調用mock方法callFoo()時,call_args訪問器仍然返回call('troz')(35~37行)。

Listing Nineteen

 1 #!/usr/bin/python
 2  
 3 from mock import Mock
 4  
 5 # The mock object
 6 class Foo(object):
 7     # instance properties
 8     _fooValue = 123
 9      
10     def callFoo(self):
11         print "Foo:callFoo_"
12      
13     def doFoo(self, argValue):
14         print "Foo:doFoo:input = ", argValue
15  
16 # create the first mock object
17 mockFoo = Mock(spec = Foo, return_value = "narf")
18 print mockFoo
19 # returns <Mock spec='Foo' id='507120'>
20 print mockFoo.call_args
21 # returns: None
22  
23 mockFoo("zort")
24 print mockFoo.call_args
25 # returns: call('zort')
26  
27 mockFoo()
28 print mockFoo.call_args
29 # returns: call()
30  
31 mockFoo("troz")
32 print mockFoo.call_args
33 # returns: call('troz')
34  
35 mockFoo.callFoo()
36 print mockFoo.call_args
37 # returns: call('troz')

訪問器call_args_list 也報告了工廠調用中已使用的參數。但是call_args返回最近使用的參數,而call_args_list返回一個列表,第一項為最早的參數。Listing Twenty顯示了這個訪問的的運用,使用了和Listing Nineteen相同的代碼。

Listing Twenty

 1 from mock import Mock
 2  
 3 # The mock object
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         print "Foo:callFoo_"
10      
11     def doFoo(self, argValue):
12         print "Foo:doFoo:input = ", argValue
13  
14 # create the first mock object
15 mockFoo = Mock(spec = Foo, return_value = "narf")
16 print mockFoo
17 # returns <Mock spec='Foo' id='507120'>
18  
19 mockFoo("zort")
20 print mockFoo.call_args_list
21 # returns: [call('zort')]
22  
23 mockFoo()
24 print mockFoo.call_args_list
25 # returns: [call('zort'), call()]
26  
27 mockFoo("troz")
28 print mockFoo.call_args_list
29 # returns: [call('zort'), call(), call('troz')]
30  
31 mockFoo.callFoo()
32 print mockFoo.call_args_list
33 # returns: [call('zort'), call(), call('troz')]

訪問器mothod_calls報告了測試對象所做的mock方法的調用。它的結果是一個列表對象,每一項顯示了方法的名稱和它的參數。

Listing Twenty-one演示了method_calls的運用。對新創建的mockFoo,method_calls返回了空列表(15~19行)。當做了工廠調用時,同樣返回空列表(21~23行)。當我調用了mock方法callFoo()時,method_calls返回一個帶一項數據的列表對象(25~27行)。當我調用doFoo(),並傳入"narf"參數時,method_calls返回帶有兩項數據的列表(29~31行)。注意每個方法名稱是按照它調用的順序顯示的。

Listing Twenty-one

 1 from mock import Mock
 2  
 3 # The mock object
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         print "Foo:callFoo_"
10      
11     def doFoo(self, argValue):
12         print "Foo:doFoo:input = ", argValue
13  
14 # create the first mock object
15 mockFoo = Mock(spec = Foo, return_value = "poink")
16 print mockFoo
17 # returns <Mock spec='Foo' id='507120'>
18 print mockFoo.method_calls
19 # returns []
20  
21 mockFoo()
22 print mockFoo.method_calls
23 # returns []
24  
25 mockFoo.callFoo()
26 print mockFoo.method_calls
27 # returns: [call.callFoo()]
28  
29 mockFoo.doFoo("narf")
30 print mockFoo.method_calls
31 # returns: [call.callFoo(), call.doFoo('narf')]
32  
33 mockFoo()
34 print mockFoo.method_calls
35 # returns: [call.callFoo(), call.doFoo('narf')]

最后一個訪問器mock_calls報告了測試對象對mock對象所有的調用。結果是一個列表,但是工廠調用和方法調用都顯示了。Listing Twenty-two演示這個訪問器的運用,使用了和Listing Twenty-one相同的代碼

Listing Twenty-two

 1 from mock import Mock
 2  
 3 # The mock object
 4 class Foo(object):
 5     # instance properties
 6     _fooValue = 123
 7      
 8     def callFoo(self):
 9         print "Foo:callFoo_"
10      
11     def doFoo(self, argValue):
12         print "Foo:doFoo:input = ", argValue
13  
14 # create the first mock object
15 mockFoo = Mock(spec = Foo, return_value = "poink")
16 print mockFoo
17 # returns <Mock spec='Foo' id='507120'>
18  
19 print mockFoo.mock_calls
20 # returns []
21  
22 mockFoo()
23 print mockFoo.mock_calls
24 # returns [call()]
25  
26 mockFoo.callFoo()>
27 print mockFoo.mock_calls
28 # returns: [call(), call.callFoo()]
29  
30 mockFoo.doFoo("narf")
31 print mockFoo.mock_calls
32 # returns: [call(), call.callFoo(), call.doFoo('narf')]
33  
34 mockFoo()
35 print mockFoo.mock_calls
36 # returns: [call(), call.callFoo(), call.doFoo('narf'), call()]

在測試中使用MOCK

數據類型,模型或者節點,這些是mock對象可能被假定的一些角色。但是mock對象怎樣適合單元測試呢?讓我們一起來看看,來自Martin Fowler的文章Mocks Aren't Stubs采取了簡化的設置。

在這個測試中,設置了三個類(圖4)。Order類是測試對象。它模擬了單一項目的采購訂單,訂單來源於一個數據源。Warehouse類是測試資源。它包含了鍵值對的序列,鍵是項目的名稱,值是可用的數量。OrderTest類是測試用例本身。

Python

圖4

Listing Twenty-three描述了Order。Order類聲明了三個屬性:項目名稱(_orderItem),要求的數量(_orderAmount)和已填寫的數量(_orderFilled)。它的構造器帶有兩個參數(8~18行),填入的屬性是_orderItem和_orderAmount。它的__repr__()方法返回了購買清單的摘要(21~24行)。

Listing Twenty-three

 1 class Order(object):
 2     # instance properties
 3     _orderItem = "None"
 4     _orderAmount = 0
 5     _orderFilled = -1
 6      
 7     # Constructor
 8     def __init__(self, argItem, argAmount):
 9         print "Order:__init__"
10          
11         # set the order item
12         if (isinstance(argItem, str)):
13             if (len(argItem) > 0):
14                 self._orderItem = argItem
15          
16         # set the order amount
17         if (argAmount > 0):
18             self._orderAmount = argAmount
19          
20     # Magic methods
21     def __repr__(self):
22        # assemble the dictionary
23         locOrder = {'item':self._orderItem, 'amount':self._orderAmount}
24         return repr(locOrder)
25      
26     # Instance methods
27     # attempt to fill the order
28     def fill(self, argSrc):
29         print "Order:fill_"
30          
31         try:
32             # does the warehouse has the item in stock?
33             if (argSrc is not None):
34                 if (argSrc.hasInventory(self._orderItem)):
35                     # get the item
36                     locCount =    argSrc.getInventory(self._orderItem, self._orderAmount)
37                  
38                     # update the following property
39                     self._orderFilled = locCount
40                 else:
41                     print "Inventory item not available"
42             else:
43                 print "Warehouse not available"
44         except TypeError:
45             print "Invalid warehouse"
46      
47     # check if the order has been filled
48     def isFilled(self):
49         print "Order:isFilled_"
50         return (self._orderAmount == self._orderFilled)

Order類定義了兩個實例方法。fill()方法從參數(argSrc)中獲取數據源。它檢查數據源是否可用,數據源的項目是否存在問題(33~34行)。它提交了一個申請並用實際返回的數量更新_orderFilled(36~39行)。當_orderAmount和_orderFilled有相同的值時,isFilled()方法返回True(48~50行)。

 

Listing Twenty-four描述了Warehouse類。它是一個抽象類,聲明了屬性和方法接口,但是沒有定義方法本身。屬性_houseName是倉庫的名字,而_houseList是它持有的庫存。還有這兩個屬性的訪問器。

Listing Twenty-four

 1 class Warehouse(object):    
 2     # private properties
 3     _houseName = None
 4     _houseList = None
 5          
 6     # accessors
 7     def warehouseName(self):
 8         return (self._houseName)
 9      
10     def inventory(self):
11         return (self._houseList)
12      
13      
14     # -- INVENTORY ACTIONS
15     # set up the warehouse
16     def setup(self, argName, argList):
17     &#9;pass
18      
19     # check for an inventory item
20     def hasInventory(self, argItem):
21         pass
22      
23     # retrieve an inventory item
24     def getInventory(self, argItem, argCount):
25         pass
26          
27     # add an inventory item
28     def addInventory(self, argItem, argCount):
29         pass

Warehouse類聲明了四個方法接口。方法setup()帶有兩個參數,是為了更新這兩個屬性。方法hasInventory()參數是項目的名稱,如果項目在庫存中則返回True。方法getInventory()的參數是項目的名稱和數量。它嘗試着從庫存中扣除數量,返回哪些是成功的扣除。方法addInventory()的參數也是項目名稱和數量。它將用這兩個參數更新_houseList。

Listing Twenty-five是測試用例本身,orderTest類。他有一個屬性fooSource是Order類所需的mock對象。setUp()方法識別執行的測試例程(14~16行),然后創建和配置mock對象(21~34行)。tearDown()方法向stdout打印一個空行。

Listing Twenty-five

 1 import unittest
 2 from mock import Mock, call
 3  
 4 class OrderTest(unittest.TestCase):
 5     # declare the test resource
 6     fooSource = None
 7      
 8     # preparing to test
 9     def setUp(self):
10         """ Setting up for the test """
11         print "OrderTest:setUp_:begin"
12          
13         # identify the test routine
14         testName = self.id().split(".")
15         testName = testName[2]
16         print testName
17          
18         # prepare and configure the test resource
19         if (testName == "testA_newOrder"):
20             print "OrderTest:setup_:testA_newOrder:RESERVED"
21         elif (testName == "testB_nilInventory"):
22             self.fooSource = Mock(spec = Warehouse, return_value = None)
23         elif (testName == "testC_orderCheck"):
24             self.fooSource = Mock(spec = Warehouse)
25             self.fooSource.hasInventory.return_value = True
26             self.fooSource.getInventory.return_value = 0
27         elif (testName == "testD_orderFilled"):
28             self.fooSource = Mock(spec = Warehouse)
29             self.fooSource.hasInventory.return_value = True
30             self.fooSource.getInventory.return_value = 10
31         elif (testName == "testE_orderIncomplete"):
32             self.fooSource = Mock(spec = Warehouse)
33             self.fooSource.hasInventory.return_value = True
34             self.fooSource.getInventory.return_value = 5
35         else:
36             print "UNSUPPORTED TEST ROUTINE"
37      
38     # ending the test
39     def tearDown(self):
40         """Cleaning up after the test"""
41         print "OrderTest:tearDown_:begin"
42         print ""
43      
44     # test: new order
45     # objective: creating an order
46     def testA_newOrder(self):
47         # creating a new order
48         testOrder = Order("mushrooms", 10)
49         print repr(testOrder)
50          
51         # test for a nil object
52         self.assertIsNotNone(testOrder, "Order object is a nil.")
53          
54         # test for a valid item name
55         testName = testOrder._orderItem
56         self.assertEqual(testName, "mushrooms", "Invalid item name")
57          
58         # test for a valid item amount
59         testAmount = testOrder._orderAmount
60         self.assertGreater(testAmount, 0, "Invalid item amount")
61      
62     # test: nil inventory
63     # objective: how the order object handles a nil inventory
64     def testB_nilInventory(self):
65         """Test routine B"""
66         # creating a new order
67         testOrder = Order("mushrooms", 10)
68         print repr(testOrder)
69          
70         # fill the order
71         testSource = self.fooSource()
72         testOrder.fill(testSource)
73          
74         # print the mocked calls
75         print self.fooSource.mock_calls
76          
77         # check the call history
78         testCalls = [call()]
79         self.fooSource.assert_has_calls(testCalls)
80      
81     # ... continued in the next listing

OrderTest類有五個測試例程。所有五個測試例程在開始的時候都創建了一個Order類的實例。例程testA_newOrder()測試Order對象是否可用是否有正確的數據(46~60行)。例程testB_nilWarehouse()創建了一個空的mock並傳入Order對象的fill()方法(64~79行)。它檢查了mock的調用歷史,確保僅僅發生了工廠調用。

 

例程testC_orderCheck()(Listing Twenty-six)測試了Order對象在庫存不足時的反應。最初,fooSource的hasInventory()方法響應True,getinventory()方法返回0。測試例程檢查是否訂單未達成,是否正確的mock方法被帶調用(16~19行)。然后測試例程創建了一個新的Order對象,這次是一個不同的項目。mock(fooSource)的方法hasInventory()的響應設置為False(第27行)。再次,例程檢查是否訂單未達成,是否調用了正確的mock方法(34~37行)。注意使用reset_mock()方法將fooSource恢復到測試前的狀態(第28行)。

Listing Twenty-six

 1 class OrderTest(unittest.TestCase):
 2     # ... see previous listing
 3      
 4     # test: checking the inventory
 5     # objective: does the order object check for inventory?
 6     def testC_orderCheck(self):
 7         """Test routine C"""
 8         # creating a test order
 9         testOrder = Order("mushrooms", 10)
10         print repr(testOrder)
11          
12         # perform the test
13         testOrder.fill(self.fooSource)
14          
15         # perform the checks
16         self.assertFalse(testOrder.isFilled())
17         self.assertEqual(testOrder._orderFilled, 0)
18          
19         self.fooSource.hasInventory.assert_called_once_with("mushrooms")
20         print self.fooSource.mock_calls
21          
22         # creating another order
23         testOrder = Order("cabbage", 10)
24         print repr(testOrder)
25          
26         # reconfigure the test resource
27         self.fooSource.hasInventory.return_value = False
28         self.fooSource.reset_mock()
29          
30         # perform the test
31         testOrder.fill(self.fooSource)
32          
33         # perform the checks
34         self.assertFalse(testOrder.isFilled())
35         self.assertEqual(testOrder._orderFilled, -1)
36          
37         self.fooSource.hasInventory.assert_called_once_with("cabbage")
38         print self.fooSource.mock_calls
39      
40     # ... continued in the next listing

測試例程testD_orderFilled()(Listing Twenty-seven)模擬了一個成功的訂單事務。fooSource的hasInventory()方法響應True,getinventory()方法返回10。例程調用fill()方法傳入mock對象,然后檢查訂單是否已完成(17~18行)。它也檢查了是否采用正確的順序和正確的參數調用了 正確的mock方法(20~24行)。

Listing Twenty-seven

 1 class OrderTest(unittest.TestCase):
 2     # ... see previous listing
 3      
 4     # test: fulfilling an order
 5     # objective: how does the order object behave with a successful transaction
 6     def testD_orderFilled(self):
 7         """Test routine D"""
 8         # creating a test order
 9         testOrder = Order("mushrooms", 10)
10         print repr(testOrder)
11          
12         # perform the test
13         testOrder.fill(self.fooSource)
14         print testOrder.isFilled()
15          
16         # perform the checks
17         self.assertTrue(testOrder.isFilled())
18         self.assertNotEqual(testOrder._orderFilled, -1)
19          
20         self.fooSource.hasInventory.assert_called_once_with("mushrooms")
21         self.fooSource.getInventory.assert_called_with("mushrooms", 10)
22          
23         testCalls = [call.hasInventory("mushrooms"), call.getInventory("mushrooms", 10)]
24         self.fooSource.assert_has_calls(testCalls)
25      
26     # ... continued in the next listing

測試例程testE_orderIncomplete()(Listing Twenty-eight)模擬了一個未完成的事務。在這個測試中,fooSource的方法hasInventory()響應True,但是getinventory()返回5。例程調用fill()方法傳入mock對象,然后檢查未完成的訂單(17~18行)。 它也檢查了是否采用正確的順序和正確的參數調用了正確的mock方法(20~25行)。

Listing Twenty-eight

 1 class OrderTest(unittest.TestCase):
 2     # ... see previous listing
 3      
 4     # test: fulfilling an order
 5     # objective: how does the order object behave with an incomplete transaction
 6     def testE_orderIncomplete(self):
 7         """Test routine E"""
 8         # creating a test order
 9         testOrder = Order("mushrooms", 10)
10         print repr(testOrder)
11          
12         # perform the test
13         testOrder.fill(self.fooSource)
14         print testOrder.isFilled()
15          
16         # perform the checks
17         self.assertFalse(testOrder.isFilled())
18         self.assertNotEqual(testOrder._orderFilled, testOrder._orderAmount)
19          
20         self.fooSource.hasInventory.assert_called_once_with("mushrooms")
21         self.fooSource.getInventory.assert_called_with("mushrooms", 10)
22         print self.fooSource.mock_calls
23          
24         testCalls = [call.hasInventory("mushrooms"), call.getInventory("mushrooms", 10)]
25         self.fooSource.assert_has_calls(testCalls)

結束語

Mocks讓我們為單元測試模擬了那些不可用或者是太龐大的資源。我們可以在運行中配置mock,在特定的測試中改變它的行為或響應,或者讓它在恰當的時候拋出錯誤和異常。

在這篇文章中,我們看到了在單元測試中設置mock的好處。我們了解了mock和fake或者stub的區別。我們了解了怎樣在Python中創建mock,怎樣管理它和用斷言跟蹤它的行為。我們研究了簡單的mock在基礎測試用例中的工作。隨意嘗試了這個測試設置並觀察它的結構。隨意的調整已提供的測試例程並觀察這些調整對測試的影響


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM