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 的測試案例(test case)寫到最後,免不了都會用上 unittest.mock 模組,原因在於有些類別、資料用 mock 的方式製造,遠遠方便於實際執行過一遍,例如測試使用者利用臉書登入,利用 mock 模擬實際臉書伺服器的回應遠比實際執行來得方便之外,也更加確保測試案例的可控制性,不會因為臉書伺服器故障或者網路問題而導致測試案例不穩定的情況發生。

然而 mock 對於初學者而言並不是十分容易理解,本文就談談 mock 中幾個重要的功能,了解這些功能之後,肯定能夠打通 mock 模組的任督二脈!

本文環境

  • Python 3.6.5

MagicMock vs Mock

翻閱 mock 文件時,一定會看到 MagicMockMock 2 個類別。

兩者的關係在於 MagicMockMock 的子類別, 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_valueside_effect 了!原則上 mock 單純的回傳值時,用 return_value 即可,若要控制 mock 拋出例外(raise Exception)或者動態變更回傳值的話,用 side_effect

reset_mock

使用 return_valueside_effect 時,如果要重置 mock (也就是清除先前使用資料等等),就需要呼叫 reset_mock(return_value=True, side_effect=True) ,因為 reset_mock 方法預設不會重置 return_valueside_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

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!