【編者按】mock是一門技術,通過偽造部分實際代碼,從而讓開發者可以驗證剩餘代碼的正確性。從而,通過mock,開發者可以非常便捷地測試某個函數的內部代碼,下面就帶你穿梭mock。
關於譯者:曾靈敏,OneAPM工程師,偶然的機會接觸到Python,為其簡潔優雅的語法所吸引,並毅然投身其中,目前在OneAPM從事Python探針研發工作。
以下為譯文
本博文主要聚焦mock的使用。mock是一門技術,通過偽造部分實際代碼,從而讓開發者可以驗證剩餘代碼的正確性。下面將通過幾個簡單的示例演示mock在Python測試代碼中的使用,以及這項極其有用的技術是如何幫助開發者改善測試代碼的。
為什麼使用mock
當進行單元測試時,目標往往是為了測試非常小的代碼塊,例如一個獨立存在的函數或類方法。換句話說,代碼測試只針對指定函數的內部代碼。如果測試代碼需要依賴於其他的代碼片段,在某種不幸的情形下,即使被測試的函數沒有變化,這部分內嵌代碼的修改仍然可能破壞原有的測試。看看下面的例子,你將豁然開朗:
[py] view plaincopy
# 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
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()
[py] view plaincopy
$ python test.py
.
Ran 1 test in 0.001s
OK
在上面的例子中,`add_and_multiply`計算兩個數的和與乘積並返回。「add_and_multiply」調用了另一個函數「multiply」進行乘積計算。如果期望摒棄「傳統「的數學,並重新定義「multiply」函數,在原有的乘積結果上加3。新的「multiply」函數如下:
[py] view plaincopy
def multiply(x, y): return x * y + 3
現在就會遇到一個問題。即使測試代碼沒有變化,需要測試的函數也沒有變化,然而,「test_add_and_multiply」卻會執行失敗:
[py] view plaincopy
$ 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進行安裝:
[py] view plaincopy
pip install mock==1.0.1
mock有多種不同的用法。我們可以用它提供猴子補丁功能,創建偽造的對象,甚至可以作為一個上下文管理器。所有這些都是基於一個共同目標的,用副本替換部分代碼來收集信息並返回偽造的響應。
mock的[ 文檔]非常密集,尋找特定的用例信息可能會非常棘手。這裡,我們就來看看一個常見的場景——替換一個內嵌函數並檢查它的輸入和輸出。
開始mock之旅
下面將使用mock重新編寫單元測試。接下來,會討論發生了什麼,以及為什麼從測試的角度來看它是非常有用的:
[py] view plaincopy
# test.py
import mock
import unittest
class MyTestCase(unittest.TestCase):
@mock.patch('multiply')
def test_add_and_multiply(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」的內部邏輯進行測試。
實現過程
乍一看,上面的語法可能不好理解。下面逐行分析:
[py] view plaincopy
@mock.patch('multiply')
def test_add_and_multiply(mock_multiply):
使用「mock.patch」裝飾器來用mock對象替換"multiply'。然後將它作為一個參數"mock_multiply"插入到測試代碼中。在這個測試的上下文中,任何對「multiply」的調用都會被重定向到「mock_multiply」對象。
有人會質疑——「怎麼能用對象替換函數!」別擔心!在Python的世界,函數也是對象。通常情況下,當我們調用「multiply()」,實際執行的是「multiply」函數的「__call__」方法。然而,恰當的使用mock,對「multiply()」的調用將執行mock對象,而不是「__call__」方法。
[py] view plaincopy
mock_multiply.return_value = 15
為了使mock函數可以返回任何東西,我們需要定義其「return_value」屬性。實際上,當mock函數被調用時,它用於定義mock對象的返回值。
[py] view plaincopy
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對象。當測試一個複雜的功能時,確保每一步都被正確調用將是一件非常令人欣慰的事情。
英文原文: Making a Mockery of Python
本文為CSDN原創,點擊「閱讀原文」可查看全文並參與討論。
如果您喜歡這篇文章,請點擊右上角「…」將本文分享給你的朋友。