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的程式碼如下:
1 2 3 4 5 |
import requests def c(url): resp = requests.get(url) # further process with resp |
如果利用mock模組,那麼就可以達到這樣的效果:使用一個mock物件替換掉上面的requests.get函式,然後執行函式c時,c呼叫requests.get的返回值就能夠由我們的mock物件來決定,而不需要伺服器的參與。簡單的說,就是我們用一個mock物件替換掉c函式和伺服器互動的過程。你一定很好奇這個功能是如何實現的,這個是mock模組內部的實現機制,不在本文的討論範圍。本文主要討論如何用mock模組來解決上面提到的這種單元測試場景。
Mock的安裝和匯入
在Python 3.3以前的版本中,需要另外安裝mock模組,可以使用pip命令來安裝:
1 |
$ sudo pip install mock |
然後在程式碼中就可以直接import進來:
1 |
import mock |
從Python 3.3開始,mock模組已經被合併到標準庫中,被命名為unittest.mock,可以直接import進來使用:
1 |
from unittest import mock |
Mock物件
基本用法
Mock物件是mock模組中最重要的概念。Mock物件就是mock模組中的一個類的例項,這個類的例項可以用來替換其他的Python物件,來達到模擬的效果。Mock類的定義如下:
1 |
class Mock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, **kwargs) |
這裡給出這個定義只是要說明下Mock物件其實就是個Python類而已,當然,它內部的實現是很巧妙的,有興趣的可以去看mock模組的程式碼。
Mock物件的一般用法是這樣的:
- 找到你要替換的物件,這個物件可以是一個類,或者是一個函式,或者是一個類例項。
- 然後例項化Mock類得到一個mock物件,並且設定這個mock物件的行為,比如被呼叫的時候返回什麼值,被訪問成員的時候返回什麼值等。
- 使用這個mock物件替換掉我們想替換的物件,也就是步驟1中確定的物件。
- 之後就可以開始寫測試程式碼,這個時候我們可以保證我們替換掉的物件在測試用例執行的過程中行為和我們預設的一樣。
舉個例子來說:我們有一個簡單的客戶端實現,用來訪問一個URL,當訪問正常時,需要返回狀態碼200,不正常時,需要返回狀態碼404。首先,我們的客戶端程式碼實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#!/usr/bin/env python # -*- coding: utf-8 -*- import requests def send_request(url): r = requests.get(url) return r.status_code def visit_ustack(): return send_request('http://www.ustack.com') |
外部模組呼叫visit_ustack()
來訪問UnitedStack的官網。下面我們使用mock物件在單元測試中分別測試訪問正常和訪問不正常的情況。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#!/usr/bin/env python # -*- coding: utf-8 -*- import unittest import mock import client class TestClient(unittest.TestCase): def test_success_request(self): success_send = mock.Mock(return_value='200') client.send_request = success_send self.assertEqual(client.visit_ustack(), '200') def test_fail_request(self): fail_send = mock.Mock(return_value='404') client.send_request = fail_send self.assertEqual(client.visit_ustack(), '404') |
- 找到要替換的物件:我們需要測試的是
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很方便。
1 2 |
client = mock.Mock() client.v2_client.get.return_value = '200' |
這個時候,你就得到了一個mock過的client例項,呼叫該例項的v2_client.get()
方法會得到的返回值是”200″。
從上面的例子中還可以看到,指定mock物件的return_value還可以使用屬性賦值的方法。
對方法呼叫進行檢查
mock物件有一些方法可以用來檢查該物件是否被呼叫過、被呼叫時的引數如何、被呼叫了幾次等。實現這些功能可以呼叫mock物件的方法,具體的可以檢視mock的文件。這裡我們舉個例子。
還是使用上面的程式碼,這次我們要檢查visit_ustack()
函式呼叫send_request()
函式時,傳遞的引數型別是否正確。我們可以像下面這樣使用mock物件。
1 2 3 4 5 6 7 8 |
class TestClient(unittest.TestCase): def test_call_send_request_with_right_arguments(self): client.send_request = mock.Mock() client.visit_ustack() self.assertEqual(client.send_request.called, True) call_args = client.send_request.call_args self.assertIsInstance(call_args[0][0], str) |
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掉一個物件。我們看個程式碼例子即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class TestClient(unittest.TestCase): def test_success_request(self): status_code = '200' success_send = mock.Mock(return_value=status_code) with mock.patch('client.send_request', success_send): from client import visit_ustack self.assertEqual(visit_ustack(), status_code) def test_fail_request(self): status_code = '404' fail_send = mock.Mock(return_value=status_code) with mock.patch('client.send_request', fail_send): from client import visit_ustack self.assertEqual(visit_ustack(), status_code) |
這個測試類和我們剛才寫的第一個測試類一樣,包含兩個測試,只不過這次不是顯示建立一個mock物件並且進行替換,而是使用了patch
函式(作為上下文管理器使用)。
patch.object
和patch
的效果是一樣的,只不過用法有點不同。舉例來說,同樣是上面這個例子,換成patch.object
的話是這樣的:
1 2 3 4 5 6 |
def test_fail_request(self): status_code = '404' fail_send = mock.Mock(return_value=status_code) with mock.patch.object(client, 'send_request', fail_send): from client import visit_ustack self.assertEqual(visit_ustack(), status_code) |
就是替換掉一個物件的指定名稱的屬性,用法和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