Python mock 模組 - 淺談 spec, return_value, side_effect, wraps - Part 2

Posted on  Dec 17, 2018  in  Python 程式設計 - 高階  by  Amo Chen  ‐ 2 min read

本文為 unittest.mock 系列文章:

Python mock 模組 - 淺談 spec, return_value, side_effect, wraps - Part 1 中介紹 unittest.mock 模組中的 return_value , side_effect 概念與用法,本文將接續介紹 specwraps

spec

預設的 Mock / MagicMock 其實存取任何屬性(甚至根本不存在的屬性)、呼叫任何方法都會回傳 1 個 mock ,雖然使用上相當方便,但是這樣的行為表現就顯得不真實。

因此 Mock 也提供 spec 參數, spec 也就是規格的意思,其功用是讓 mock 盡可能地按照規格模仿,試圖取得不在規格內的屬性或者方法,就拋出 AttributeError ,如此一來可避免 mock 預設的特性造成的困擾,測試案例的 mock 就更加嚴謹。

目前 spec 參數可代入 object 或者都是字串的 list 。如果是 object , mock 就會自行解讀該 object 內的屬性與方法,如果是字串 list ,那麼每一個字串元素都會被視為 mock 中的屬性,可以參考範例。

以下為預設 Mock 與有使用 spec 的 Mock 比較,可以發現沒有 spec 的情況下, mock 存取任意屬性與呼叫任何方法都不會有問題,而有使用 spec 的 Mock 遇到 spec 不支援的屬性或方法時就會拋出 AttributeError :

from unittest.mock import Mock


mock_without_spec = Mock()
mock_without_spec.attribute_a
mock_without_spec.attribute_b
mock_without_spec.print_attribute_a()
mock_without_spec.print_attribute_b()


class TheSpec(object):

    NAME = 'spec'

    def __init__(self):
        self.attribute_a = 'a'

    def print_attribute_a(self):
        print(self.attribute_a)


mock_with_spec = Mock(spec=TheSpec())
mock_with_spec.attribute_a
mock_with_spec.attribute_b  # AttributeError

mock_with_spec.print_attribute_a()
mock_with_spec.print_attribute_b()  # AttributeError

mock_with_spec = Mock(spec=['NAME', 'attribute_a', 'print_attribute_a'])
mock_with_spec.NAME
mock_with_spec.print_attribute_a()
mock_with_spec.attribute_b  # AttributeError

spec_set

談完 spec 就能夠進一步理解 spec_set

有使用 spec 的 mock 試圖取得(get)不在規格內的屬性時,會拋出 AttributeError ,但還是可以設定(set)不在規格內的屬性或方法。

而使用 spec_set 的情況下,則是連設定(set)不在規格內的屬性或方法時都會拋出 AttributeError ,可以說是相當嚴格的設定。

以下範例直接展示 2 者的差別:

from unittest.mock import Mock


class TheSpec(object):

    NAME = 'spec'

    def __init__(self):
        self.attribute_a = 'a'

    def print_attribute_a(self):
        print(self.attribute_a)


mock_with_spec = Mock(spec=TheSpec())
mock_with_spec.attribute_b = 'b'

mock_with_spec_set = Mock(spec_set=TheSpec())
mock_with_spec_set.attribute_b = 'b'  # AttributeError

wraps

目前為止談到的 mock 都是虛假的 object ,用來取代原本受測程式中的特定 object 。

那麼有沒有一種 mock ,在 mock 的同時還能讓原本模仿的對象實際執行?答案是,有!

這項行為被稱為 wraps ,簡單說就是用 mock 將模仿的對象包起來,在呼叫 mock 的時候, mock 也會以相同的參數呼叫被包起來的 object ,讓該 object 實際執行。

例如以下範例 mock_with_wraps 在呼叫 print_attribute_a('b', 'c') 時,參數 'b', 'c' 被代入 TheSpec 物件中的 print_attribute_a 方法,所以執行結果會印出 a ('b', 'c')

from unittest.mock import Mock


class TheSpec(object):

    NAME = 'spec'

    def __init__(self):
        self.attribute_a = 'a'

    def print_attribute_a(self, *args):
        print(self.attribute_a, args)
        return True


mock_with_wraps = Mock(wraps=TheSpec())
assert mock_with_wraps.print_attribute_a('b', 'c') is True  # will print a ('b', 'c')

這就是 wraps 的功用。

需要注意的是同時設定 return_valuewraps 的話, mock 會忽略 wraps 的設定,直接傳回 return_value

If the mock has an explicit return_value set then calls are not passed to the wrapped object and the return_value is returned instead.

總結

經過 2 篇文章說明之後,相信大家對於 mock 模組中的 spec, return_value, side_effect, wraps 也不會過於陌生才是,未來在撰寫測試案例(test case)時,也一定能夠使用合適的設定。

總之, Happy Coding!

References

https://docs.python.org/3/library/unittest.mock.html

對抗久坐職業傷害

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

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

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

贊助我們的創作

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

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