當前位置:
首頁 > 知識 > 讀懂Python的Mock對象庫(1)

讀懂Python的Mock對象庫(1)

當你希望編寫健壯的代碼時,使用測試對於驗證程序邏輯是否正確,可靠以及高效至關重要。然而,測試的價值取決於它能達成這些標準的程度。比如複雜的邏輯和預料之外的依賴等障礙都會對編寫高質量的測試造成困難。Python中的mock對象庫unittest.mock可以幫助你解決這些障礙。

讀完本篇文章,你將收穫很多:

1. 使用Mock創建Python mock對象

2. 斷言你使用的對象是你期望的

3. 校驗並使用Python mock的數據

4. 針對Python mock對象進行配置

你將從mock是什麼以及它如何優化你的測試程序開始。

Mock是什麼

一個在測試環境中模仿並替換真實對象的mock對象,它是一個強大且功能豐富的工具,可以提高你的測試代碼質量。

使用Python模擬對象的一個原因是在測試期間控制代碼的行為。

例如,如果你的代碼對外發出了一個HTTP請求,那麼只有服務的運行符合你的預期,測試程序才能按照你的預定方式執行。有時候,外部服務的臨時變更有可能會導致你的測試組件出現間歇性故障。

因此,最好是在可控的環境中運行你的測試。使用mock對象替換實際請求可以允許你以可預知的方式模擬外部請求服務的中斷以及成功。

有時,你很難測試到代碼庫的特定區域。這些區域包括except和if語句塊等難以滿足條件的部分。使用Python mock對象可以幫助你控制你的代碼執行路徑以進入這些區域,並提高你的測試代碼覆蓋率。

使用mock對象的另一個原因是為了更好的理解你代碼中實際使用的對象。你可以發現,Python mock對象包含的數據的用處:

1. 如果你調用了方法

2. 你怎樣調用的這個方法

3. 你多久調用一次這個方法

了解mock對象的作用是學習使用mock對象的第一步。

現在,你將學習如何使用Python mock對象。

Python mock庫

Python的mock對象庫是unittest.mock。它提供了一種在測試中引入mock的簡單方式。

註:Python 3.3以及之後的版本在標準庫中添加了unittest.mock。如果你使用的是較早版本的Python,你需要安裝官方的代碼資源庫。這裡,你需要從PyPI安裝mock:

讀懂Python的Mock對象庫(1)

unittest.mock提供了一個Mock類,它可以模擬你代碼庫中的實際對象。Mock可以提供極其靈活準確的數據,它與它的子類可以滿足你在測試中遇到的大多數Python mock需求。

unittest.mock庫還提供了一個叫做patch()的函數,它可以用Mock對象的實例替換實際的對象。你可以將patch()用作裝飾器或上下文管理器,從而控制mock對象的使用範圍。一旦退出了指定範圍,patch()就會使用原始對象替換並清除你代碼中的mock對象。

最後,unittest.mock為mock對象中的常規問題提供了一些解決方案。

現在,你已經對什麼是mock以及你將使用哪些庫有了更深的理解。我們來深入探討一下unittest.mock提供的功能和方法。

Mock對象

unittest.mock為mock對象提供了一個基類Mock。因為Mock非常靈活,它的用例實際上並沒有多少限制。

首先實例化一個新的Mock實例:

讀懂Python的Mock對象庫(1)

現在可以使用新建的Mock來替換你代碼中的對象。你可以通過將其作為參數傳遞給對象或者重新定義另一個對象來完成這個操作:

讀懂Python的Mock對象庫(1)

當你替換代碼中的對象時,Mock必須模擬的與實際對象相像才可以。否則,Mock無法替換原始對象。

舉個例子,假設你正在模擬json庫並使用程序調用dumps()方法,那麼你的Python mock對象就必須包含dumps()方法。

接下來,你將了解到Mock是如何解決這個問題的。

惰性成員及方法

Mock的實例必須模擬它所需要替換的對象。為了靈活的實現這種方式,Mock會在訪問這些屬性的時候創建它們。

讀懂Python的Mock對象庫(1)

由於Mock可以動態的創建任意屬性,所以它適用於替換所有對象。

看一個之前的例子,如果你mock一個json庫並調用dumps()方法,那麼Python mock對象為了讓自身的介面能夠匹配庫里的介面,會創建這個方法。

讀懂Python的Mock對象庫(1)

請注意這兩個版本中dumps()的兩個重要特徵:

1. 與真正的dumps()不同,mock出來的方法不需要任何參數。實際上,它可以接收你傳遞給它的任何參數。

2. dumps()方法的返回值同樣也是一個Mock對象。Mock能夠以遞歸的方式定義其他的mock對象,以供你在更複雜的情況下使用。

讀懂Python的Mock對象庫(1)

因為每個mock方法返回的也是一個Mock對象,所以你可以通過多種方式使用mock。

Mock保持靈活的同時也提供了足夠的信息。接下來,你將會學習到如何使用mock更好的理解你的代碼。

斷言檢查

Mock實例存儲了如何使用它們的信息。例如,你可以看到你是否調用了一個方法,你怎樣調用的這個方法,等等。這裡有兩種主要的方式使用到了這些信息。

第一,你可以斷言你的程序使用的對象是你預期的那樣:

讀懂Python的Mock對象庫(1)

使用.assert_called()確保你調用了方法,而.assert_called_once()確保你只調用了一次這個方法。

這兩個方法都有衍生的其他方法,便於你檢查傳遞給mock方法的參數:

1. .assert_called_with(*args, **kwarge)

2. .assert_called_once_with(*args, **kwarge)

你必須使用與傳遞給實際方法相同的參數調用mock的方法,斷言才會通過。

讀懂Python的Mock對象庫(1)

json.loads.assert_called_with("{"key": "value"}")拋出了一個AssertionError異常信息,因為你原本希望使用一個位置參數調用loads(),但是實際上你使用了一個關鍵字參數調用了該方法。json.loads.assert_called_with(s="{"key": "value"}")這樣才是正確的。

第二,你可以查看Mock的特殊屬性來了解應用程序如何使用對象:

讀懂Python的Mock對象庫(1)

你可使用這些屬性編寫測試程序,來確保你代碼中的對象按預期執行。

現在你可以創建mock對象並檢查其使用的數據。接下來我們將介紹如何自定義mock方法來更多的應用於你的測試環境。

管理Mock的返回數據

使用mock的一個原因是在測試期間控制代碼的行為。一種方式是指定函數的返回值。我們來舉個例子說明一下它是怎麼工作的。

首先,創建一個名為my_calendar.py的文件,添加一個使用了Python的datetime庫的方法is_weekday(),判斷今天是否為工作日。最後寫一個測試,斷言函數按照預期工作。

讀懂Python的Mock對象庫(1)

因為測試的是今天是否為工作日,所以結果取決於運行測試的那天:

讀懂Python的Mock對象庫(1)

如果這個命令運行沒有輸出結果,那麼斷言是成功的。不幸的是,如果你在周末運行了這個命令,你將看到一個AssertionError錯誤。

讀懂Python的Mock對象庫(1)

開始寫測試時,確保結果的可預見性是很重要的。你可以在測試過程中使用Mock排除不確定因素。下面的例子中,你可以mock一個datetime並且通過使用.return_value為.today()選擇一個你希望設置的日期。

讀懂Python的Mock對象庫(1)

這個例子中,.today()是一個mock出來的方法。你已經通過mock中的.return_value指定一個特定的日期消除不一致性。這樣,當你調用.today()時會返回你指定的日期datetime。

在第一個測試中,確定了星期二是工作日。第二次測試,確定星期六不是工作日。現在,你運行測試的時候是哪一天不重要了,因為已經可以mockdatetime並控制對象的行為。

進一步閱讀:儘管這樣通過Mock對象來mock一個datetime的方式是一個不錯的例子,但是已經有一個非常棒的庫freezegun可以mock日期datetime了。

在構建測試時,通常因為函數要比簡單的單項邏輯更為複雜,你可能會遇到mock的函數返回值不夠用的情況。

有時,你可能會多次調用函數以期可以返回不同的值甚至是拋出異常。你可能使用.side_effect實現這一點。

管理Mock的側面影響

你可以通過指定mock的函數的側面影響來控制代碼的行為。.side_effect定義了當你調用mock的函數是會發生什麼。

在my_calendar.py中增加一個函數來測試這是怎麼工作的:

讀懂Python的Mock對象庫(1)

get_holidays()向本地伺服器發起了一個訪問一組假期的請求。如果伺服器響應成功,get_holidays()會返回一個字典,否則函數返回None。

你可通過設置requests.get.side_effect來測試get_holidays()是如何響應連接超時的。

對於這個例子,你只能看到my_calendar.py中的部分相關代碼。可以使用unittest庫來構建測試用例。

讀懂Python的Mock對象庫(1)

用.assertRaises()驗證在設置了側面影響的get()函數之後get_holidays()是不是會引發異常。

運行並查看測試結果:

讀懂Python的Mock對象庫(1)

如果你希望更加動態的載入,可以將.side_effect設置為Mock調用是mock的函數。這個函數會分配side_effect的參數和返回值。

讀懂Python的Mock對象庫(1)

首先,創建.log_request(),它接收一個URL,使用print()列印輸出,然後返回一個mock的響應。之後,使用.side_effect為.get()設置.log_request(),在調用get_holidays()時會調用這個方法。當你開始運行測試時,可以看到.get()將自己的參數轉發給.log_request()然後接受返回值並返回:

讀懂Python的Mock對象庫(1)

很棒,print()語句列印了正確的值。而且get_holidays()也返回了假字典。

.side_effect還可以迭代。迭代的話必須包含返回值,異常或者兩者都有。每次調用mock的方法時,這個迭代會產生下一個值。比如,你可以在Timeout之後重試並返回一個成功的響應。

讀懂Python的Mock對象庫(1)

第一次調用get_holidays(),.get()會引發一個Timeout異常。第二次則會返回一個有效的假字典。這些側面影響會匹配傳遞給.side_effect的列表的順序。

你可以直接在Mock對象上設置.return_value和.side_effect。但是Python mock對象希望能靈活的創建其屬性,所以還有很好的方式配置這些以及其他設置。

配置你的Mock

你可以配置Mock來控制對象的某些行為。Mock中可配置的成員包括.side_effect,.return_value以及.name。當你創建或者使用.configure_mock()時會配置Mock。

你可以初始化是通過指定某些屬性來配置Mock:

讀懂Python的Mock對象庫(1)

雖然可以在Mock實例上設置 .side_effect和.return_value,但是像.name等其他屬性只能通過.__init__()或者.configure_mock()來設置。如果你嘗試在Mock實例上設置.name,你得到結果會不一樣:

讀懂Python的Mock對象庫(1)

.name是要使用的對象的公共屬性。因此,Mock不允許你以與 .side_effect或.return_value相同的方式來設置這個值。如果你訪問mock.name,將會創建一個.name屬性而不是配置mock對象。

你可以使用.configure_mock()配置一個已有的Mock實例:

讀懂Python的Mock對象庫(1)

通過將字典解包到.configure_mock()或Mock.__init__(),你甚至可以配置Python mock對象的屬性。使用Mock配置,可以簡化之前的例子:

讀懂Python的Mock對象庫(1)

現在,你可以創建和配置Python mock對象了。你還可以使用mock控制你的應用程序的行為。目前,你已經使用mock作為函數的參數或者在與測試中相同的模塊中匹配對象。

接下來,你將學習如何將mock對象替換為其他模塊中的實際對象。

patch()

unittest.mock為mock對象提供了一種強大的名為patch()的機制,它可以在給定模塊中查找對象並用Mock替換該對象。

通常,使用patch()作為裝飾器或者上下文管理器來提供一個mock目標對象的範圍。

patch()作為裝飾器

如果你要在整個測試函數的持續時間內使用mock對象,可以使用patch()作為裝飾器。

將邏輯代碼和測試放入單獨的文件重新組織my_calendar.py文件,可以看到其工作原理:

讀懂Python的Mock對象庫(1)

這些函數現在都在自己的文件中,與測試區分開來。接下來,將在tests.py文件中重新創建測試。

到目前為止,你已經在對象各自所在的文件中打了"猴子補丁"。猴子補丁在運行時將一個對象替換為另一個對象。現在,將使用patch()替換my_calendar.py中的對象:

讀懂Python的Mock對象庫(1)

最初,你在本地範圍內創建了Mock對象並對requests打了補丁。現在你需要從tests.py中訪問my_calendar.p中的requests庫。

對於這種情況,使用了patch()作為裝飾器並傳遞目標對象的路徑。目標路徑為"my_calendar.requests",它由模塊名稱和對象組成。

你還為測試函數定義了一個新的參數,patch()使用這個參數將mock對象傳遞到測試中。這樣可以根據斷言修改mock或者斷言。

你可以執行這個測試模塊確保符合預期:

讀懂Python的Mock對象庫(1)

技術細節:patch()會返回一個繼承自Mock的MagicMock的實例。MagicMock對你很有幫助,因為它使用合理的默認值實現了大量的魔術方法,諸如.__len__(), .__str__(), 及 .__iter__()。

使用patch()作為裝飾器在這個例子中可以正常運行。在某些情況下,使用patch()作為上下文管理器更具有可讀性,更有效且更容易。

patch()作為上下文管理器

有時,你會希望使用patch()作為上下文管理器而不是裝飾器。你更傾向於上下文管理器的原因可能包括:

1. 你只想在測試的一部分範圍內mock一個對象

2. 你已經使用了太多的裝飾器或參數,降低了測試的可讀性

想要patch()作為上下文管理器,你可以使用with語句:

讀懂Python的Mock對象庫(1)

當你退出with語句時,patch()將mock對象再替換回原始對象。

現在,你都是mock的完整的對象,但是有時候你可能僅僅希望mock對象的一部分。

為對象的屬性打補丁

假設你只希望mock一個對象的方法而不是整個對象。你可以使用patch.object()來完成此操作。

比如,對象方法.test_get_holidays_timeout()實際上只是需要mock一個requests.get()並將.side_effect設置為Timeout。

讀懂Python的Mock對象庫(1)

這個例子中,你只mock了get()方法而不是整個requests包,其他的屬性還是與原來相同。

object()採用和patch()相同的配置參數。但是,將目標本身作為第一個參數傳遞而不是傳遞目標的路徑。第二個參數是你嘗試mock的目標對象的屬性。你還可以使用object()作為類似於patch()的上下文管理器。

進一步閱讀:除了對象和屬性,你還可以使用patch.dict()來將patch()作用於字典。

學習如何使用patch()對於mock其他模塊中的對象非常重要。然而,有時候需要mock的目標對象的路徑並不明顯。

在哪裡打補丁

有一個很重要的點是告知patch()去哪裡查找你需要mock的對象,因為如果你選擇了一個錯誤的目標位置,那麼patch()的結果可能出乎你的預料。

假設你在my_calendar.py使用patch()mock函數is_weekday():

讀懂Python的Mock對象庫(1)

首先導入my_calendar.py,然後用Mock替換is_weekday()。很好,運行符合預期。

現在,我們稍微改變一下這個例子,並直接引用函數:

讀懂Python的Mock對象庫(1)

注意:根據你閱讀本教程的日期,你從控制台看到的輸出可能是Trus或False。重要的是輸出與之前的Mock不同。

請注意,即使傳遞給patch()的目標位置沒有更改,調用is_weekday()的結果也不同,這是由於導入函數的方式不同。

from my_calendar import is_weekday意思是將實際函數引入到本地文件範圍。因此,即使你稍後使用patch()操作了該函數,之後也會忽略已有的mock,因為你已經持有對未mock函數的本地引用。

使用patch()的好的法則是查找這個對象。

在第一個例子中,mock"my_calendar.is_weekday()"是生效的,因為你是在模塊中查找該函數。第二個例子中,你持有對is_weekday()的本地引用,由於是在本地範圍內找到了函數,所以mock的是本地的函數:

讀懂Python的Mock對象庫(1)

現在,你已經深刻領略了patch()的強大之處。並且知道如何對對象和屬性使用patch()並對為其打補丁。

接下來,你將看到mock對象的一些常規問題以及unittest.mock提供的解決方案。


英文原文:https://realpython.com/python-mock-library/ 譯者:敦偉

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python部落 的精彩文章:

VS Code中的Python –2019年3月發布
讀懂Python的Mock對象庫(2)

TAG:Python部落 |