python mock實踐筆記
前言
如果你寫程式碼時會寫單元測試(unit test,UT),那麼多半會遇到想要將某個函式隔離開,去掉外部依賴的情況,例如這個函式依賴其他函式的返回或者依賴某個API呼叫的返回。這種情況下就一定繞不開mock這項技術。本文並不打算介紹python下mock的方方面面,只會寫我個人實際使用中覺得比較實用的部分。
mocking是什麼?
mock就是模擬的意思,mocking主要用在單元測試中,當被測試的物件依賴另一個複雜的物件時,我們需要模擬這個複雜的物件的行為,mocking就是建立這個物件,模擬它的行為。
基本用法
unittest.mock
模組包含了mock相關的功能。
Mock
物件
看如下一段程式碼:
from unittest.mock import Mock
# 建立一個mock物件
mock = Mock()
print(mock)
# 結果:<Mock id='140668914327224'> 表明此處的mock是一個Mock物件
Mock非常靈活,當我們訪問一個Mock物件的某個屬性時,這個屬性如果不存在會被自動建立,看如下程式碼:
# 在訪問之前mock並沒有some_attribute這個屬性 print(mock.some_attribute) # 結果:<Mock name='mock.some_attribute' id='140348173848360'> # 可見,在訪問的時候建立了該屬性 print(mock.do_something) # 結果:<Mock name='mock.do_something' id='140348173886128'>
正是由於此特性,Mock可以用來模擬任意物件。
下面從最基本的開始介紹:
設定返回值和屬性
Mock物件可以返回常量,也可以隨著輸入返回不同值。
返回常量-return_value
mock = Mock() # 設定返回值 mock.return_value = 3 print(mock()) # 返回 3 # 設定方法的返回值 mock.method.return_value = 3 mock.method() # 返回 3 # 在建構函式中設定返回值 mock = Mock(return_value=3) mock() # 返回 3 # 設定屬性 mock = Mock() mock.x = 3 mock.x # 返回 3
返回隨著輸入變化-side_effect
side_effect
也算一個屬性,當你不滿足指定一個常量返回時,就會期望用上它。
# 1.將side_effect設定為一個異常類
mock = Mock(side_effect=Exception('Boom!'))
mock() # 呼叫時就會丟擲異常
from requests.exceptions import Timeout
requests = Mock()
requests.get.side_effect = Timeout # 模擬API超時
with self.assertRaises(Timeout):
# get_holidays函式裡面呼叫了requests.get,那麼將會捕獲到Timeout異常
get_holidays()
# 將會丟擲異常
# 2.將side_effect設定為一個迭代器(場景:mock物件被多次呼叫,每次返回值不一樣)
mock = MagicMock(side_effect=[4, 5, 6])
mock()
4
mock()
5
mock()
6
# 3.將side_effect設定為一個函式(場景:返回值由輸入引數決定)
vals = {(1, 2): 1, (2, 3): 2}
def side_effect(*args):
return vals[args]
mock = MagicMock(side_effect=side_effect)
mock(1, 2)
1
mock(2, 3)
2
mock一個類
def some_function():
instance = module.Foo()
return instance.method()
# 模擬Foo這個類
with patch('module.Foo') as mock:
# 此處的“mock”就是一個類,mock.return_value代表該類返回的例項(instance)
instance = mock.return_value
# 模擬例項方法的返回(此處的方法名就叫method)
instance.method.return_value = 'the result'
# 函式中對Foo的呼叫就會使用模擬類
result = some_function()
assert result == 'the result'
模擬一個物件(object)的方法(method)
patch
patch可能是使用最多的方法,它的使用場景:
1.模擬一個類的屬性
2.模擬一個模組的屬性
如果我們測試的函式在同一個檔案中可以不使用patch,patch主要用在測試程式碼和主程式碼分離的情況下。
有3種裝飾器可用:
# patch的第一個引數是一個string,形式:package.module.Class.attribute,以此指定要模擬的屬性,第二個引數是可選的,第一個引數裡面的屬性將被替換為該值。
@patch('package.module.attribute', sentinel.attribute)
# 例子一,傳有第二個引數:
mock = MagicMock(return_value=sentinel.file_handle)
with patch('builtins.open', mock):
handle = open('filename', 'r')
# 例子二,不傳第二個引數:
# 不傳第二個引數時,mock物件會被傳入在函式的引數裡,如下,並且注意順序:
class MyTest(unittest.TestCase):
@patch('package.module.ClassName1')
@patch('package.module.ClassName2')
def test_something(self, MockClass2, MockClass1):
self.assertIs(package.module.ClassName1, MockClass1)
self.assertIs(package.module.ClassName2, MockClass2)
# 例子三,使用as,將會獲得一個引用
with patch('ProductionClass.method') as mock_method:
mock_method.return_value = None
real = ProductionClass()
real.method(1, 2, 3)
# 例子四,將path裝飾在類上,作用於每個測試函式
@patch('mymodule.SomeClass')
class MyTest(unittest.TestCase):
def test_one(self, MockSomeClass):
self.assertIs(mymodule.SomeClass, MockSomeClass)
def test_two(self, MockSomeClass):
self.assertIs(mymodule.SomeClass, MockSomeClass)
# 裝飾在類上只針對test開頭的函式,此處不是test開頭,不傳遞MockSomeClass引數
def not_a_test(self):
return 'something'
# 例子五,另一種在整個類中模擬的辦法
class MyTest(unittest.TestCase):
def setUp(self):
patcher = patch('mymodule.foo')
self.addCleanup(patcher.stop)
self.mock_foo = patcher.start()
def test_foo(self):
self.assertIs(mymodule.foo, self.mock_foo)
# patch的類在同一個檔案中
# 此處需要使用__main__,代表當前模組
@patch('__main__.SomeClass')
# patch.object第一個引數是一個物件,第二個引數是該物件的屬性名稱,第三個是可選的,第二個引數裡面的屬性將被替換為該值。
# 場景:只模擬部分屬性而非整個物件
@patch.object(SomeClass, 'attribute', sentinel.attribute)
@patch.dict()
如下為例子,供參考、copy:
# my_calendar.py
import requests
from datetime import datetime
def is_weekday():
today = datetime.today()
# Python's datetime library treats Monday as 0 and Sunday as 6
return (0 <= today.weekday() < 5)
def get_holidays():
r = requests.get('http://localhost/api/holidays')
if r.status_code == 200:
return r.json()
return None
# tests.py
import unittest
from my_calendar import get_holidays
from requests.exceptions import Timeout
from unittest.mock import patch
class TestCalendar(unittest.TestCase):
# patch裝飾在函式上如果函式裡面會呼叫my_calendar下的requests函式,就會被mock掉
@patch('my_calendar.requests')
def test_get_holidays_timeout(self, mock_requests):
mock_requests.get.side_effect = Timeout
with self.assertRaises(Timeout):
get_holidays()
mock_requests.get.assert_called_once()
if __name__ == '__main__':
unittest.main()
mock可以直接使用裝飾器,也可以使用上下文管理器,為什麼使用上下文管理器?一般原因有如下兩個,可自行判斷要不要用:
1.只想針對部分程式碼,而不是整個測試函式
2.patch裝飾器已經很多了,裝飾器太多影響可讀性
patch路徑應該是什麼?
where to patch?並不是要去引用某個函式本身所在的位置,而是要看這個函式在哪裡使用的,如果在使用了的地方有import,那麼就應該是那個地方的路徑。
舉例:
一個檔案中(這個檔案路徑:package2.m2.py):
from package1.m1 import fun1
def fun2():
fun1()
在另一個測試檔案中:
class JustTest(TestCase):
@patch('package2.m2.fun1') # 這才是正確的路徑,而不是package1.m1.fun1
def test_fun2(self, mock_fun1):
mock_fun1.return_value = 3