Python mock 模組 - 淺談 spec, return_value, side_effect, wraps - Part 1
Posted on Dec 16, 2018 in Python 程式設計 - 高階 by Amo Chen ‐ 3 min read
本文為 unittest.mock 系列文章:
- Python mock 模組 - 淺談 spec, return_value, side_effect, wraps - Part 1
- Python mock 模組 - 淺談 spec, return_value, side_effect, wraps - Part 2
Python 的測試案例(test case)寫到最後,免不了都會用上 unittest.mock 模組,原因在於有些類別、資料用 mock 的方式製造,遠遠方便於實際執行過一遍,例如測試使用者利用臉書登入,利用 mock 模擬實際臉書伺服器的回應遠比實際執行來得方便之外,也更加確保測試案例的可控制性,不會因為臉書伺服器故障或者網路問題而導致測試案例不穩定的情況發生。
然而 mock 對於初學者而言並不是十分容易理解,本文就談談 mock 中幾個重要的功能,了解這些功能之後,肯定能夠打通 mock 模組的任督二脈!
本文環境
- Python 3.6.5
MagicMock vs Mock
翻閱 mock 文件時,一定會看到 MagicMock 與 Mock 2 個類別。
兩者的關係在於 MagicMock
是 Mock
的子類別, MagicMock
實作 Mock
類別絕大多數的 magic methods ,例如 __hash__
, __repr__
, __setattr__
, __getattr__
等等。
以下範例能夠知道 2 者差別:
>>> from unittest.mock import Mock, MagicMock
>>>
>>> int(Mock())
TypeError: int() argument must be a string, a bytes-like object or a number, not 'Mock'
>>>
>>> int(MagicMock())
1
原則上如果測試案例不需要客製任何 magic methods 的話,只要使用 MagicMock
就可以了。
return_value
回到主題,最簡單的 mock 就是直接替換掉回傳值即可,所以 mock 最簡單的用法就是設定 return_value
:
from random import randint
from unittest.mock import MagicMock, patch
def get_n():
return randint(0, 100)
def if_func_return_value_great_than_50(f):
return f() > 50
if_func_return_value_great_than_50(get_n)
mock = MagicMock(return_value=51)
assert if_func_return_value_great_than_50(mock) is True
上述範例利用 MagicMock(return_value=51)
製造一個會永遠 return 51 的 mock ,所以我們就能夠將 mock 代入 if_func_return_value_great_than_50
取代原本的 get_n()
,讓測試案例永遠都能夠一直固定行為,避免 randint 的影響。
而除了利用 mock 代入 if_func_return_value_great_than_50
之外,還可以利用 patch 直接將 randint 改為 mock ,讓我們能夠控制 get_n()
的行為,也就是下方範例的 patch('__main__.randint', return_value=51)
:
from random import randint
from unittest.mock import MagicMock, patch
def get_n():
return randint(0, 100)
def if_func_return_value_great_than_50(f):
return f() > 50
with patch('__main__.randint', return_value=51):
print(get_n())
assert True is if_func_return_value_great_than_50(get_n)
經過上述 2 個範例,應能夠了解 mock 中 return_value 的實際功用與 patch 的用途。
side_effect
由於 return_value 的定位是 mock 物件被呼叫時的回傳值,所以它也沒辦法支援比較複雜的用法,例如 raise Exception 或者動態決定回傳值,所以 side_effect 被用來彌補 return_value 的不足。
所以 side_effect
可以接受帶入 function, iterable 甚至 exception class ,讓我們當作更複雜的 return_value 使用:
from random import randint
from unittest.mock import MagicMock, patch
def get_n_or_raise_value_error_if_n_eq_zero():
n = randint(0, 100)
if n == 0:
raise ValueError('zero')
return n
def if_func_return_value_great_than_50():
try:
return get_n_or_raise_value_error_if_n_eq_zero() > 50
except ValueError:
print('got zero')
with patch('__main__.randint', side_effect=ValueError):
if_func_return_value_great_than_50()
另外,官方文件中也提到使用 side_effect 時,如果沒有設定 DEFAULT 時, side_effect 的回傳值就會被當作 return_value ,也就是說同時設定 return_value 與 side_effect 的情況下, side_effect 的回傳值會蓋掉 return_value !
A function to be called whenever the Mock is called. Useful for raising exceptions or dynamically changing return values. The function is called with the same arguments as the mock, and unless it returns DEFAULT, the return value of this function is used as the return value.
from random import randint
from unittest.mock import MagicMock, patch
def get_n():
return randint(0, 100)
def if_func_return_value_great_than_50(f):
return f() > 50
def get_n_side_effect(start, stop):
return 1
with patch('__main__.randint', return_value=51, side_effect=get_n_side_effect):
print(get_n())
assert True is if_func_return_value_great_than_50(get_n)
上述範例執行結果如下,可以看到 side_effect 蓋掉 return_value 設定,導致 randint 始終回傳值為 1 :
1
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
<ipython-input-62-62a7f391aeae> in <module>()
13 with patch('__main__.randint', return_value=51, side_effect=get_n_side_effect):
14 print(get_n())
---> 15 assert True is if_func_return_value_great_than_50(get_n)
AssertionError:
至此,我們已認識 return_value
與 side_effect
了!原則上 mock 單純的回傳值時,用 return_value
即可,若要控制 mock 拋出例外(raise Exception)或者動態變更回傳值的話,用 side_effect
。
reset_mock
使用 return_value
與 side_effect
時,如果要重置 mock (也就是清除先前使用資料等等),就需要呼叫 reset_mock(return_value=True, side_effect=True)
,因為 reset_mock
方法預設不會重置 return_value
與 side_effect
的設定。請參考以下範例:
from random import randint
from unittest.mock import MagicMock, patch
def get_n():
return randint(0, 100)
def if_func_return_value_great_than_50(f):
return f() > 50
if_func_return_value_great_than_50(get_n)
mock = MagicMock(return_value=51)
assert if_func_return_value_great_than_50(mock) is True
mock.reset_mock()
assert if_func_return_value_great_than_50(mock) is True
mock.reset_mock(return_value=True)
# TypeError: '>' not supported between instances of 'MagicMock' and 'int'
assert if_func_return_value_great_than_50(mock) is True
上述範例會在最後一行拋出 TypeError
,因為mock.reset_mock(return_value=True)
會真正將 return_value
重置,造成測試案例無法順利比較 mock 的回傳值是否大於 50 。
小結
下一篇我們將介紹剩下的 spec 與 wraps !
References
https://docs.python.org/3/library/unittest.mock.html