[轉載]Python Mock的入門
轉載自segmentfault Python Mock的入門
Mock是什麼
Mock這個詞在英語中有模擬的這個意思,因此我們可以猜測出這個庫的主要功能是模擬一些東西。準確的說,Mock是Python中一個用於支持單元測試的庫,它的主要功能是使用mock對象替代掉指定的Python對象,以達到模擬對象的行為。簡單的說,mock庫用於如下的場景:
假設你開發的項目叫a,裡面包含了一個模塊b,模塊b中的一個函數c(也就是a.b.c)在工作的時候需要調用發送請求給特定的伺服器來得到一個JSON返回值,然後根據這個返回值來做處理。如果要為a.b.c函數寫一個單元測試,該如何做?
一個簡單的辦法是搭建一個測試的伺服器,在單元測試的時候,讓a.b.c函數和這個測試伺服器交互。但是這種做法有兩個問題:
- 測試伺服器可能很不好搭建,或者搭建效率很低。
- 你搭建的測試伺服器可能無法返回所有可能的值,或者需要大量的工作才能達到這個目的。
那麼如何在沒有測試伺服器的情況下進行上面這種情況的單元測試呢?Mock模塊就是答案。上面已經說過了,mock模塊可以替換Python對象。我們假設a.b.c的代碼如下:
import requestsnndef c(url):n resp = requests.get(url)n # further process with respn
如果利用mock模塊,那麼就可以達到這樣的效果:使用一個mock對象替換掉上面的requests.get函數,然後執行函數c時,c調用requests.get的返回值就能夠由我們的mock對象來決定,而不需要伺服器的參與。簡單的說,就是我們用一個mock對象替換掉c函數和伺服器交互的過程。你一定很好奇這個功能是如何實現的,這個是mock模塊內部的實現機制,不在本文的討論範圍。本文主要討論如何用mock模塊來解決上面提到的這種單元測試場景。
Mock的安裝和導入
在Python 3.3以前的版本中,需要另外安裝mock模塊,可以使用pip命令來安裝:
$ sudo pip install mockn
然後在代碼中就可以直接import進來:
import mockn
從Python 3.3開始,mock模塊已經被合併到標準庫中,被命名為unittest.mock,可以直接import進來使用:
from unittest import mockn
Mock對象
基本用法
Mock對象是mock模塊中最重要的概念。Mock對象就是mock模塊中的一個類的實例,這個類的實例可以用來替換其他的Python對象,來達到模擬的效果。Mock類的定義如下:
class Mock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, **kwargs)n
這裡給出這個定義只是要說明下Mock對象其實就是個Python類而已,當然,它內部的實現是很巧妙的,有興趣的可以去看mock模塊的代碼。
Mock對象的一般用法是這樣的:
- 找到你要替換的對象,這個對象可以是一個類,或者是一個函數,或者是一個類實例。
- 然後實例化Mock類得到一個mock對象,並且設置這個mock對象的行為,比如被調用的時候返回什麼值,被訪問成員的時候返回什麼值等。
- 使用這個mock對象替換掉我們想替換的對象,也就是步驟1中確定的對象。
- 之後就可以開始寫測試代碼,這個時候我們可以保證我們替換掉的對象在測試用例執行的過程中行為和我們預設的一樣。
舉個例子來說:我們有一個簡單的客戶端實現,用來訪問一個URL,當訪問正常時,需要返回狀態碼200,不正常時,需要返回狀態碼404。首先,我們的客戶端代碼實現如下:
#!/usr/bin/env python # -*- coding: utf-8 -*- import requestsnnndef send_request(url):n r = requests.get(url)n return r.status_codennndef visit_ustack(): n return send_request(http://www.ustack.com)n
外部模塊調用visit_ustack()來訪問UnitedStack的官網。下面我們使用mock對象在單元測試中分別測試訪問正常和訪問不正常的情況。
#!/usr/bin/env python n# -*- coding: utf-8 -*-nnimport unittestnnimport mocknnimport clientnnnclass TestClient(unittest.TestCase): def test_success_request(self):n success_send = mock.Mock(return_value=200)n client.send_request = success_sendn self.assertEqual(client.visit_ustack(), 200)nn def test_fail_request(self):n fail_send = mock.Mock(return_value=404)n client.send_request = fail_sendn self.assertEqual(client.visit_ustack(), 404)n
- 找到要替換的對象:我們需要測試的是visit_ustack這個函數,那麼我們需要替換掉send_request這個函數。
- 實例化Mock類得到一個mock對象,並且設置這個mock對象的行為。在成功測試中,我們設置mock對象的返回值為字元串「200」,在失敗測試中,我們設置mock對象的返回值為字元串"404"。
- 使用這個mock對象替換掉我們想替換的對象。我們替換掉了client.send_request
- 寫測試代碼。我們調用client.visit_ustack(),並且期望它的返回值和我們預設的一樣。
上面這個就是使用mock對象的基本步驟了。在上面的例子中我們替換了自己寫的模塊的對象,其實也可以替換標準庫和第三方模塊的對象,方法是一樣的:先import進來,然後替換掉指定的對象就可以了。
稍微高級點的用法
class Mock的參數
上面講的是mock對象最基本的用法。下面來看看mock對象的稍微高級點的用法(並不是很高級啊,最完整最高級的直接去看mock的文檔即可,後面給出)。
先來看看Mock這個類的參數,在上面看到的類定義中,我們知道它有好幾個參數,這裡介紹最主要的幾個:
- name: 這個是用來命名一個mock對象,只是起到標識作用,當你print一個mock對象的時候,可以看到它的name。
- return_value: 這個我們剛才使用過了,這個欄位可以指定一個值(或者對象),當mock對象被調用時,如果side_effect函數返回的是DEFAULT,則對mock對象的調用會返回return_value指定的值。
- side_effect: 這個參數指向一個可調用對象,一般就是函數。當mock對象被調用時,如果該函數返回值不是DEFAULT時,那麼以該函數的返回值作為mock對象調用的返回值。
其他的參數請參考官方文檔。
mock對象的自動創建
當訪問一個mock對象中不存在的屬性時,mock會自動建立一個子mock對象,並且把正在訪問的屬性指向它,這個功能對於實現多級屬性的mock很方便。
client = mock.Mock()nclient.v2_client.get.return_value = 200n
這個時候,你就得到了一個mock過的client實例,調用該實例的v2_client.get()方法會得到的返回值是"200"。
從上面的例子中還可以看到,指定mock對象的return_value還可以使用屬性賦值的方法。
對方法調用進行檢查
mock對象有一些方法可以用來檢查該對象是否被調用過、被調用時的參數如何、被調用了幾次等。實現這些功能可以調用mock對象的方法,具體的可以查看mock的文檔。這裡我們舉個例子。
還是使用上面的代碼,這次我們要檢查visit_ustack()函數調用send_request()函數時,傳遞的參數類型是否正確。我們可以像下面這樣使用mock對象。
class TestClient(unittest.TestCase):nn def test_call_send_request_with_right_arguments(self):n client.send_request = mock.Mock()n client.visit_ustack()n self.assertEqual(client.send_request.called, True)n call_args = client.send_request.call_argsn self.assertIsInstance(call_args[0][0], str)n
Mock對象的called屬性表示該mock對象是否被調用過。
Mock對象的call_args表示該mock對象被調用的tuple,tuple的每個成員都是一個mock.call對象。mock.call這個對象代表了一次對mock對象的調用,其內容是一個tuple,含有兩個元素,第一個元素是調用mock對象時的位置參數(*args),第二個元素是調用mock對象時的關鍵字參數(**kwargs)。
現在來分析下上面的用例,我們要檢查的項目有兩個:
- visit_ustack()調用了send_request()
- 調用的參數是一個字元串
patch和patch.object
在了解了mock對象之後,我們來看兩個方便測試的函數:patch和patch.object。這兩個函數都會返回一個mock內部的類實例,這個類是class _patch。返回的這個類實例既可以作為函數的裝飾器,也可以作為類的裝飾器,也可以作為上下文管理器。使用patch或者patch.object的目的是為了控制mock的範圍,意思就是在一個函數範圍內,或者一個類的範圍內,或者with語句的範圍內mock掉一個對象。我們看個代碼例子即可:
class TestClient(unittest.TestCase):n def test_success_request(self):n status_code = 200n success_send = mock.Mock(return_value=status_code)n with mock.patch(client.send_request, success_send):n from client import visit_ustackn self.assertEqual(visit_ustack(), status_code)nn def test_fail_request(self):n status_code = 404n fail_send = mock.Mock(return_value=status_code)n with mock.patch(client.send_request, fail_send):n from client import visit_ustackn self.assertEqual(visit_ustack(), status_code)n
這個測試類和我們剛才寫的第一個測試類一樣,包含兩個測試,只不過這次不是顯示創建一個mock對象並且進行替換,而是使用了patch函數(作為上下文管理器使用)。
patch.object和patch的效果是一樣的,只不過用法有點不同。舉例來說,同樣是上面這個例子,換成patch.object的話是這樣的:
def test_fail_request(self):n status_code = 404n fail_send = mock.Mock(return_value=status_code)n with mock.patch.object(client, send_request, fail_send):n from client import visit_ustackn self.assertEqual(visit_ustack(), status_code)n
就是替換掉一個對象的指定名稱的屬性,用法和setattr類似。
如何學習使用mock?
你肯定很奇怪,本文不就是教人使用mock的么?其實不是的,我發現自己在學習mock的過程中遇到的主要困難是不清楚mock能做什麼,而不是mock對象到底有哪些函數。因此寫這篇文章的主要目的是為了說明mock能做什麼。
當你知道了mock能做什麼之後,要如何學習並掌握mock呢?最好的方式就是查看閱讀官方文檔,並在自己的單元測試中使用。
最後,學習mock技能你應該要能夠感受到一種控制的快感,就是你能享受控制外部服務的快樂。當你感受到這種快感的時候,你的mock應該就達到熟練使用的水平了。
官方文檔
Python 2.7
mock還未加入標準庫。
http://www.voidspace.org.uk/python/mock/index.html
Python 3.4
mock已經加入了標準庫。
https://docs.python.org/3.4/library/unittest.mock-examples.html https://docs.python.org/3.4/library/unittest.mock.html
推薦閱讀:
※python連接資料庫集群進行自動化運維
※python寫的CGI腳本,用print為什麼不是列印到控制台,而是發送到客戶端?
※(一)Python密碼學之旅---02古典密碼之凱撒密碼推廣
※Scrapy爬蟲框架教程(四)-- 抓取AJAX非同步載入網頁
※通過聚類分析吃雞亞洲,北美,歐洲前百名玩家的行為