Mock這個詞在英語中有模擬的這個意思,所以咱們能夠猜想出這個庫的主要功能是模擬一些東西。準確的說,Mock是Python中一個用於支持單元測試的庫,它的主要功能是使用mock對象替代掉指定的Python對象,以達到模擬對象的行爲。簡單的說,mock庫用於以下的場景:html
假設你開發的項目叫a,裏面包含了一個模塊b,模塊b中的一個函數c(也就是a.b.c)在工做的時候須要調用發送請求給特定的服務器來獲得一個JSON返回值,而後根據這個返回值來作處理。若是要爲a.b.c函數寫一個單元測試,該如何作?python
一個簡單的辦法是搭建一個測試的服務器,在單元測試的時候,讓a.b.c函數和這個測試服務器交互。可是這種作法有兩個問題:ruby
測試服務器可能很很差搭建,或者搭建效率很低。服務器
你搭建的測試服務器可能沒法返回全部可能的值,或者須要大量的工做才能達到這個目的。ide
那麼如何在沒有測試服務器的狀況下進行上面這種狀況的單元測試呢?Mock模塊就是答案。上面已經說過了,mock模塊能夠替換Python對象。咱們假設a.b.c的代碼以下:函數
import requests def c(url): resp = requests.get(url) # further process with resp
若是利用mock模塊,那麼就能夠達到這樣的效果:使用一個mock對象替換掉上面的requests.get函數,而後執行函數c時,c調用requests.get的返回值就可以由咱們的mock對象來決定,而不須要服務器的參與。簡單的說,就是咱們用一個mock對象替換掉c函數和服務器交互的過程。單元測試
在Python 3.3之前的版本中,須要另外安裝mock模塊,可使用pip命令來安裝:測試
$ sudo pip install mock
而後在代碼中就能夠直接import進來:編碼
import mock
從Python 3.3開始,mock模塊已經被合併到標準庫中,被命名爲unittest.mock,能夠直接import進來使用:url
from unittest import 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)
這裏給出這個定義只是要說明下Mock對象其實就是個Python類而已,固然,它內部的實現是很巧妙的,有興趣的能夠去看mock模塊的代碼。
mock主要有name,return_value,side_effect,和spec四個函數。
Mock對象的通常用法是這樣的:
找到你要替換的對象,這個對象能夠是一個類,或者是一個函數,或者是一個類實例。
而後實例化Mock類獲得一個mock對象,而且設置這個mock對象的行爲,好比被調用的時候返回什麼值,被訪問成員的時候返回什麼值等。
使用這個mock對象替換掉咱們想替換的對象,也就是步驟1中肯定的對象。
以後就能夠開始寫測試代碼,這個時候咱們能夠保證咱們替換掉的對象在測試用例執行的過程當中行爲和咱們預設的同樣。
舉個例子來講:咱們有一個簡單的客戶端實現,用來訪問一個URL,當訪問正常時,須要返回狀態碼200,不正常時,須要返回狀態碼404。首先,咱們的客戶端代碼實現以下:
#!/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對象在單元測試中分別測試訪問正常和訪問不正常的狀況。
#!/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進來,而後替換掉指定的對象就能夠了。
上面講的是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很方便。
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對象。
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()
調用的參數是一個字符串
在瞭解了mock對象以後,咱們來看兩個方便測試的函數:patch
和patch.object
。這兩個函數都會返回一個mock內部的類實例,這個類是class _patch
。返回的這個類實例既能夠做爲函數的裝飾器,也能夠做爲類的裝飾器,也能夠做爲上下文管理器。使用patch
或者patch.object
的目的是爲了控制mock的範圍,意思就是在一個函數範圍內,或者一個類的範圍內,或者with
語句的範圍內mock掉一個對象。咱們看個代碼例子便可:
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
的話是這樣的:
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.patch('mymodule.sys')
@mock.patch('mymodule.os')
@mock.patch('mymodule.os.path')
def test_something(self, mock_os_path, mock_os, mock_sys):
pass
注意到了嗎,咱們的裝飾方法的參數是反向匹配的? 這是有部分緣由是由於Python的工做方式。下面是使用多個裝飾方法的時候,實際的代碼執行順序:
patch_sys(patch_os(patch_os_path(test_something)))
因爲這個關於sys的補丁在最外層,所以會在最後被執行,使得它成爲實際測試方法的最後一個參數。請特別注意這一點,而且在作測試使用調試器來保證正確的參數按照正確的順序被注入。
示例一
1》定義modular.py文件,內容以下:
# coding=utf-8
#設置編碼,utf-8可支持中英文
class Count():
def add(self):
pass
2》定義mock_demo01.py文件,內容以下:
# coding=utf-8
#設置編碼,utf-8可支持中英文
import mock
import unittest
from modular import Count
# test Count class
class TestCount(unittest.TestCase):
def test_add(self):
#首先,調用被測試類Count()
count = Count()
#經過Mock類模擬被調用的方法add()方法,return_value 定義add()方法的返回值。
count.add = mock.Mock(return_value=13)
#接下來,至關於在正常的調用add()方法,傳兩個參數8和5,而後會獲得相加的結果13。而後,13的結果是咱們在上一步就預先設定好的。
result = count.add(8,5)
#最後,經過assertEqual()方法斷言,返回的結果是不是預期的結果13。
self.assertEqual(result,13)
if __name__ == '__main__':
unittest.main()
示例二
1》定義modular.py文件,內容以下:
# coding=utf-8
#設置編碼,utf-8可支持中英文
class Count():
def add(self, a, b):
return a + b
2》定義mock_demo02.py文件,內容以下:
# coding=utf-8
#設置編碼,utf-8可支持中英文
import mock
import unittest
from modular import Count
# test Count class
class TestCount(unittest.TestCase):
def test_add(self):
#首先,調用被測試類Count()
count = Count()
#side_effect參數和return_value是相反的。它給mock分配了可替換的結果,覆蓋了return_value。
## 簡單的說,一個模擬工廠調用將返回side_effect值,而不是return_value。
#因此,設置side_effect參數爲Count類add()方法,那麼return_value的做用失效。
count.add = mock.Mock(return_value=13, side_effect=count.add)
#此次將會真正的調用add()方法,獲得的返回值爲16(8+8)。經過print打印結果。
result = count.add(8, 8)
print(result)
#檢查mock方法是否得到了正確的參數。
count.add.assert_called_with(8, 8)
##最後,經過assertEqual()方法斷言,返回的結果是不是預期的結果16。
self.assertEqual(result, 16)
if __name__ == '__main__':
unittest.main()
示例三
1》定義function.py文件,內容以下:
# coding=utf-8
#設置編碼,utf-8可支持中英文
def add_and_multiply(x, y):
addition = x + y
multiple = multiply(x, y)
return (addition, multiple)
def multiply(x, y):
return x * y+3
2》定義func_test.py文件,內容以下:
# coding=utf-8
#設置編碼,utf-8可支持中英文
from mock import patch
import unittest
import function
class MyTestCase(unittest.TestCase):
"""
patch()裝飾/上下文管理器能夠很容易地模擬類或對象在模塊測試。
在測試過程當中,您指定的對象將被替換爲一個模擬(或其餘對象),並在測試結束時還原。
這裏模擬function.py文件中multiply()函數。
在定義測試用例中,將mock的multiply()函數(對象)重命名爲 mock_multiply對象。
"""
@patch("function.multiply")
def test_add_and_multiply(self, mock_multiply):
x = 3
y = 5
#設定mock_multiply對象的返回值爲固定的15。
mock_multiply.return_value = 15
#在此以前已經模擬function文件中multiply方法的返回值爲15,所以下面執行過程當中addition, multiple的值分別是8和15
addition, multiple = function.add_and_multiply(x, y)
#檢查ock_multiply方法的輸入參數是否與上面方法調用時候function文件中multiply方法的輸入參數一致。
mock_multiply.assert_called_once_with(3, 5)
self.assertEqual(8, addition)
self.assertEqual(15, multiple)
if __name__ == "__main__":
unittest.main()
mock還未加入標準庫。
http://www.voidspace.org.uk/python/mock/index.html
mock已經加入了標準庫。
https://docs.python.org/3.4/library/unittest.mock-examples.html
https://docs.python.org/3.4/library/unittest.mock.html