標籤:
原文地址:http://engineroom.trackmaven.com/blog/making-a-mockery-of-python/
今天我們來談論下mock的使用。當然,請不要誤會,這裡的mock可不是嘲弄的意思。mock是一門技術,通過偽造部分實際代碼,從而讓我們能夠驗證剩餘代碼的正確性。現在我們將通過幾個簡單的樣本示範mock在Python測試代碼中的使用,以及這項極其有用的技術是如何協助我們改善測試代碼的。
為什麼我們需要mock?
當我們進行單元測試的時候,我們的目標往往是為了測試非常小的代碼塊,例如一個獨立存在的函數或類方法。換句話說,我們只需要針對那個函數內部的代碼進行測試。如果測試代碼依賴於其他的程式碼片段,即使被測試的函數沒有變化,我們會發現在某種不幸的情形下,這部分內嵌代碼的修改可能會破壞原有的測試。看看下面的例子,你將豁然開朗:
# function.pydef add_and_multiply(x, y): addition = x + y multiple = multiply(x, y) return (addition, multiple)def multiply(x, y): return x * y# test.pyimport unittestfrom function import add_and_multiplyclass 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() $ python test.py.----------------------------------------------------------------------Ran 1 test in 0.001sOK
在上面的例子中,add_and_multiply計算兩個數的和與乘積並返回。add_and_multiply調用了另一個函數multiply進行乘積計算。
假設我們想要摒棄“傳統“的數學,並重新定義multiply函數,在原有的乘積結果上加3。
新的multiply函數如下:
def multiply(x, y): return x * y + 3
現在我們遇到一個問題。我們的測試代碼沒有變化,我們想要測試的函數也沒有變化,然而,
test_add_and_multiply卻會執行失敗:
$ python test.pyF======================================================================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.001sFAILED (failures=1)
這個問題之所以會發生,是因為我們的原始測試代碼並非真正的單元測試。儘管我們想要測試的是外部函數,但我們隱性的將內建函式也包含進來,因為我們期望的結果是依賴於這個內建函式的行為的。雖然在上面簡單的樣本中呈現的差異顯得毫無意義,但某些情境下,我們需要測試一個複雜的邏輯代碼塊 - 例如,一個Django視圖函數基於某些特定條件調用各種不同的內部功能,從函數調用結果中分離出視圖邏輯的測試就顯得尤為重要了。
解決這個問題有兩種方案。我們要麼忽略它,像整合測試那樣去進行單元測試,要麼求助於mock。第一種方案的缺點是,整合測試僅僅告訴我們函數調用時哪一行代碼出問題了,這樣更難找到問題根源所在。這並不是說,整合測試沒有用處,因為在某些情況下它確實非常有用。不管怎樣,單元測試和整合測試用於解決不同的問題,它們應該被同時使用。因此,如果我們想要成為一個好的測試人員,我們會選擇另一種方案:mock。
mock是什嗎?
mock是一個極其優秀的Python包,Python 3已將其納入標準庫。對於我們這些還在UnicodeError遍布的Python 2.x中掙紮的苦逼碼農,可以通過pip進行安裝:
pip install mock==1.0.1
mock有多種不同的用法。我們可以用它提供猴子補丁功能,建立偽造的對象,甚至可以作為一個上下文管理器。所有這些都是基於一個共同目標的,用副本替換部分代碼來收集資訊並返回偽造的響應。
mock的文檔非常密集,尋找特定的用例資訊可能會非常棘手。這裡,我們就來看看一個常見的情境 - 替換一個內嵌函數並檢查它的輸入和輸出。
開始mock之旅
讓我們用mock來重新編寫單元測試。接下來,我們將討論發生了什麼,以及為什麼從測試的角度來看它是非常有用的:
# test.pyimport mockimport unittestfrom function import add_and_multiplyclass 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的內部邏輯進行測試。
剛才我們做了什嗎?
咋一看,上面的文法可能不好理解。讓我們逐行分析:
@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__方法。
mock_multiply.return_value = 15
為了使mock函數可以返回任何東西,我們需要定義其return_value屬性。實際上,當mock函數被調用時,它用於定義mock對象的傳回值。
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對象。當測試一個複雜的功能時,確保每一步都被正確調用將是一件非常令人欣慰的事情。
本文系OneAPM工程師編譯整理。OneAPM是中國基礎軟體領域的新興領軍企業,能協助企業使用者和開發人員輕鬆實現:緩慢的程式碼和SQL語句的即時抓取。想閱讀更多技術文章,請訪問OneAPM官方技術部落格。
【譯】Python中如何建立mock?