今天我們來談論下mock的使用。當然,請不要誤會,這裡的mock可不是嘲弄的意思。mock是一門技術,通過偽造部分實際程式碼,從而讓我們能夠驗證剩餘程式碼的正確性。現在我們將通過幾個簡單的示例演示mock在Python測試程式碼中的使用,以及這項極其有用的技術是如何幫助我們改善測試程式碼的。
為什麼我們需要mock?
當我們進行單元測試的時候,我們的目標往往是為了測試非常小的程式碼塊,例如一個獨立存在的函式或類方法。換句話說,我們只需要針對那個函式內部的程式碼進行測試。如果測試程式碼依賴於其他的程式碼片段,即使被測試的函式沒有變化,我們會發現在某種不幸的情形下,這部分內嵌程式碼的修改可能會破壞原有的測試。看看下面的例子,你將豁然開朗:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# function.py def add_and_multiply(x, y): addition = x + y multiple = multiply(x, y) return (addition, multiple) def multiply(x, y): return x * y # test.py import unittest from function import add_and_multiply class MyTestCase(unittest.TestCase): def test_add_and_multiply(self): x = 3 y = 5 addition, multiple = add_and_multiply(x, y) self.assertEqual(8, addition) self.assertEqual(15, multiple) if __name__ == "__main__": unittest.main() |
1 2 3 4 5 6 |
$ python test.py . ---------------------------------------------------------------------- Ran 1 test in 0.001s OK |
在上面的例子中,add_and_multiply
計算兩個數的和與乘積並返回。add_and_multiply
呼叫了另一個函式multiply
進行乘積計算。
假設我們想要摒棄“傳統“的數學,並重新定義multiply
函式,在原有的乘積結果上加3。
新的multiply
函式如下:
1 2 3 |
def multiply(x, y): return x * y + 3 |
現在我們遇到一個問題。我們的測試程式碼沒有變化,我們想要測試的函式也沒有變化,然而,test_add_and_multiply
卻會執行失敗:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ python test.py F ====================================================================== FAIL: test_add_and_multiply (__main__.MyTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "test.py", line 13, in test_add_and_multiply self.assertEqual(15, multiple) AssertionError: 15 != 18 ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1) |
這個問題之所以會發生,是因為我們的原始測試程式碼並非真正的單元測試。儘管我們想要測試的是外部函式,但我們隱性的將內部函式也包含進來,因為我們期望的結果是依賴於這個內部函式的行為的。雖然在上面簡單的示例中呈現的差異顯得毫無意義,但某些場景下,我們需要測試一個複雜的邏輯程式碼塊 – 例如,一個Django檢視函式基於某些特定條件呼叫各種不同的內部功能,從函式呼叫結果中分離出檢視邏輯的測試就顯得尤為重要了。
解決這個問題有兩種方案。我們要麼忽略它,像整合測試那樣去進行單元測試,要麼求助於mock。第一種方案的缺點是,整合測試僅僅告訴我們函式呼叫時哪一行程式碼出問題了,這樣更難找到問題根源所在。這並不是說,整合測試沒有用處,因為在某些情況下它確實非常有用。不管怎樣,單元測試和整合測試用於解決不同的問題,它們應該被同時使用。因此,如果我們想要成為一個好的測試人員,我們會選擇另一種方案:mock。
mock是什麼?
mock是一個極其優秀的Python包,Python 3已將其納入標準庫。對於我們這些還在UnicodeError遍佈的Python 2.x中掙扎的苦逼碼農,可以通過pip進行安裝:
1 |
pip install mock==1.0.1 |
mock有多種不同的用法。我們可以用它提供猴子補丁功能,建立偽造的物件,甚至可以作為一個上下文管理器。所有這些都是基於一個共同目標的,用副本替換部分程式碼來收集資訊並返回偽造的響應。
mock的文件非常密集,尋找特定的用例資訊可能會非常棘手。這裡,我們就來看看一個常見的場景 – 替換一個內嵌函式並檢查它的輸入和輸出。
開始mock之旅
讓我們用mock來重新編寫單元測試。接下來,我們將討論發生了什麼,以及為什麼從測試的角度來看它是非常有用的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# test.py import mock import unittest from function import add_and_multiply class MyTestCase(unittest.TestCase): @mock.patch('function.multiply') def test_add_and_multiply(self, mock_multiply): x = 3 y = 5 mock_multiply.return_value = 15 addition, multiple = add_and_multiply(x, y) mock_multiply.assert_called_once_with(3, 5) self.assertEqual(8, addition) self.assertEqual(15, multiple) if __name__ == "__main__": unittest.main() |
至此,我們可以改變multiply
函式來做任何我們想做的 – 它可能返回加3後的乘積,返回None,或返回favourite line from Monty Python and the Holy Grail – 你會發現,我們上面的測試仍然可以通過。這是因為我們mock了multiply
函式。在真正的單元測試場景下,我們並不關心multiply
函式內部發生了什麼,從測試add_and_multiply
的角度來看,我們只關心multiply
被正確的引數呼叫了。這裡我們假定有另一個單元測試會針對multiply
的內部邏輯進行測試。
剛才我們做了什麼?
咋一看,上面的語法可能不好理解。讓我們逐行分析:
1 2 |
mock.patch('function.multiply') def test_add_and_multiply(self, mock_multiply): |
我們使用mock.patch
裝飾器來用mock物件替換multiply
。然後,我們將它作為一個引數mock_multiply
插入到我們的測試程式碼中。在這個測試的上下文中,任何對multiply
的呼叫都會被重定向到mock_multiply
物件。
有人會質疑 – “怎麼能用物件替換函式!?“別擔心!在Python的世界,函式也是物件。通常情況下,當我們呼叫multiply()
,我們實際執行的是multiply
函式的__call__
方法。然而,恰當的使用mock,對multiply()
的呼叫將執行我們的mock物件而不是__call__
方法。
1 |
mock_multiply.return_value = 15 |
為了使mock函式可以返回任何東西,我們需要定義其return_value
屬性。實際上,當mock函式被呼叫時,它用於定義mock物件的返回值。
1 2 3 |
addition, multiple = add_and_multiply(x, y) mock_multiply.assert_called_once_with(3, 5) |
在測試程式碼中,我們呼叫了外部函式add_and_multiply
。它會呼叫內嵌的multiply
函式,如果我們正確的進行了mock,呼叫將會被我們定義的mock物件取代。為了驗證這一點,我們可以用到mock物件的高階特性 – 當它們被呼叫時,傳給它們的任何引數將被儲存起來。顧名思義,mock物件的assert_called_once_with
方法就是一個不錯的捷徑來驗證某個物件是否被一組特定的引數呼叫過。如果被呼叫了,測試通過。反之,assert_called_once_with
會丟擲AssertionError
的異常。
我們從中學到了什麼?
好吧,我們遇到了很多實際問題。首先,我們通過mock將multiply
函式從add_and_multiply
中分離出來。這就意味著我們的單元測試只針對add_and_multiply
的內部邏輯。只有針對add_and_multiply
的程式碼修改將影響測試的成功與否。
其次,我們現在可以控制內嵌函式的輸出,以確保外部函式處理了不同的情況。例如,add_and_multiply
可能有邏輯條件依賴於multiply
的返回值:比如說,我們只想在乘積大於10的條件下返回一個值。通過人為設定multiply
的返回值,我們可以模擬乘積小於10的情況以及乘積大於10的情況,從而可以很容易測試我們的邏輯正確性。
最後,我們現在可以驗證被mock的函式被呼叫的次數,並傳入了正確的引數。由於我們的mock物件取代了multiply
函式的位置,我們知道任何針對multiply
函式的呼叫都會被重定向到該mock物件。當測試一個複雜的功能時,確保每一步都被正確呼叫將是一件非常令人欣慰的事情。