淺析Mock,F(xiàn)ake和Stub在測(cè)試中的應(yīng)用

自動(dòng)化測(cè)試中梦湘,我們常會(huì)使用一些經(jīng)過簡(jiǎn)化的瞎颗,行為與表現(xiàn)類似于生產(chǎn)環(huán)境下的對(duì)象的復(fù)制品。引入這樣的復(fù)制品能夠降低構(gòu)建測(cè)試用例的復(fù)雜度捌议,允許我們獨(dú)立而解耦地測(cè)試某個(gè)模塊哼拔,不再擔(dān)心受到系統(tǒng)中其他部分的影響。

在《The Art of Unit Testing》書中Mock 被描述為假對(duì)象瓣颅,通過驗(yàn)證是否發(fā)生與對(duì)象的交互來幫助確定測(cè)試是否失敗或通過倦逐。其他的東西都被定義為Stub。在這本書中宫补,F(xiàn)ake對(duì)象就是不真實(shí)的檬姥,根據(jù)它們的使用情況曾我,它們可以是Stub,也可以是Mock健民。

更復(fù)雜一點(diǎn)的定義是Gerard Meszaros 在XunitPatterns中對(duì)此類對(duì)象的定義抒巢。他對(duì)這類對(duì)象統(tǒng)一稱呼為:Test Double。包含:Dummy秉犹,F(xiàn)ake蛉谜,Spy,Mock和Stub崇堵。

Test Double種類.png

而通常型诚,測(cè)試人員更傾向于使用 Mock 來統(tǒng)一描述不同的 Test Doubles。

不過對(duì)于 Test Doubles 實(shí)現(xiàn)的誤解還是可能會(huì)影響到測(cè)試的設(shè)計(jì)筑辨,使測(cè)試用例變得混亂和脆弱俺驶,最終帶來不必要的重構(gòu)。CC先生就最常用的Mock棍辕,F(xiàn)ake和Stub來解釋一下不同的 Double 的使用場(chǎng)景暮现。

Fake:We use a Fake Object to replace the functionality of a real DOC in a test for reasons other than verification of indirect inputs and outputs of the SUT. Typically, it implements the same functionality as the real DOC but in a much simpler way. While a Fake Object is typically built specifically for testing, it is not used as either a control point or a observation point by the test.
簡(jiǎn)單的來說,F(xiàn)ake 是那些包含了生產(chǎn)環(huán)境下具體實(shí)現(xiàn)的簡(jiǎn)化版本的對(duì)象楚昭。

比如在測(cè)試系統(tǒng)時(shí)需要頻繁的連接數(shù)據(jù)庫進(jìn)行操作栖袋,而此時(shí)有可能數(shù)據(jù)庫還沒有完全實(shí)現(xiàn),我們就可以采用快速編寫系統(tǒng)原型抚太,并且基于內(nèi)存存儲(chǔ)來運(yùn)行整個(gè)系統(tǒng)塘幅,推遲有關(guān)數(shù)據(jù)庫設(shè)計(jì)所用到的一些決定來加速測(cè)試環(huán)境的搭建。另一個(gè)常見的使用場(chǎng)景就是利用 Fake 來保證在測(cè)試環(huán)境下支付永遠(yuǎn)返回成功結(jié)果尿贫。

Stub:Test stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
Stub只是返回一個(gè)規(guī)定的值电媳,而不會(huì)去涉及到系統(tǒng)的任何改變。

比較常見的場(chǎng)景就是系統(tǒng)希望去查詢某一類的信息庆亡,而Stub可以總是返回一個(gè)固定值匾乓,比如發(fā)送郵件的功能,Stub可以總是返回郵件發(fā)送成功的標(biāo)識(shí)1又谋,但是你并不知道你到底發(fā)送了郵件給誰或者發(fā)送了幾封郵件拼缝。

Mock:We can use a Mock Object as an observation point that is used to verify the indirect outputs of the SUT as it is exercised. Typically, the Mock Object also includes the functionality of a Test Stub in that it must return values to the SUT if it hasn't already failed the tests but the emphasisis on the verification of the indirect outputs. Therefore, a Mock Object is lot more than just a Test Stub plus assertions; it is used a fundamentally different way.

就算在Gerard Meszaros的定義里面我們可以看出Mock和Stub有一定的重合性,比較大的區(qū)別是Mock專注于observation point彰亥,而Stub專注于control point咧七,或者從另一個(gè)角度上面來說,Mock是會(huì)有行為的更改任斋,而Stub只是狀態(tài)的一個(gè)變化而已继阻。


在Python 3.3以前的版本中,需要另外安裝mock模塊,可以使用pip命令來安裝

pip install mock

使用的時(shí)候直接導(dǎo)入即可:

import mock

從Python 3.3開始瘟檩,mock模塊已經(jīng)被合并到標(biāo)準(zhǔn)庫中犬第,被命名為unittest.mock,可以直接import進(jìn)來使用:

from unittest import mock

也就是說我們以后使用Python的時(shí)候不用導(dǎo)入任何的第三方包就可以方便使用Mock來模擬測(cè)試對(duì)象的芒帕。Python中的Mock是非常容易使用,可以說是在unittest中使用最多丰介。 模擬是基于“動(dòng)作 - >斷言”模式背蟆,而不是許多Mock框架使用的“記錄 - >重放”。

Mock的基礎(chǔ)使用

Mock對(duì)象的一般用法是這樣的:

  1. 找到你要替換的對(duì)象哮幢,這個(gè)對(duì)象可以是一個(gè)類带膀,或者是一個(gè)函數(shù),或者是一個(gè)類實(shí)例橙垢。
  2. 實(shí)例化Mock類得到一個(gè)mock對(duì)象垛叨,并且設(shè)置這個(gè)mock對(duì)象的行為,比如被調(diào)用的時(shí)候返回什么值柜某,被訪問成員的時(shí)候返回什么值等嗽元。
  3. 使用這個(gè)mock對(duì)象替換掉我們想替換的對(duì)象,也就是步驟1中確定的對(duì)象喂击。

之后就可以開始寫測(cè)試代碼剂癌,這個(gè)時(shí)候我們可以保證我們替換掉的對(duì)象在測(cè)試用例執(zhí)行的過程中行為和我們預(yù)設(shè)的一樣。

舉個(gè)例子: 簡(jiǎn)單定義一個(gè)Person類翰绊,其中的代碼為:

class Person:
    def __init__(self):
        self.__age = 10
        
    def get_fullname(self, first_name, last_name):
        return first_name + ' ' + last_name
        
    def get_age(self):
        return self.__age
        
    @staticmethod
    def get_class_name():
        return Person.__name__

類里有兩個(gè)成員方法佩谷,一個(gè)有參數(shù),一個(gè)無參數(shù)监嗜,還有一個(gè)靜態(tài)方法

1). 使用Mock類谐檀,返回固定值
新建一個(gè)文件叫MockPerson.py,來測(cè)試:

from unittest import mock
import unittest
from .person import Person


class PersonTest(unittest.TestCase):
    def test_should_get_age(self):
        p = Person()

        # 不mock時(shí)裁奇,get_age應(yīng)該返回10
        self.assertEqual(p.get_age(), 10)

        # mock掉get_age方法桐猬,讓它返回20
        p.get_age = mock.Mock(return_value=20)
        self.assertEqual(p.get_age(), 20)

    def test_should_get_fullname(self):
        p = Person()

        # mock掉get_fullname,讓它返回'Tracy Cheng'
        p.get_fullname = mock.Mock(return_value='Tracy cheng')
        self.assertEqual(p.get_fullname(), 'Tracy cheng')

if __name__ == '__main__':
    unittest.main()

返回固定值時(shí)框喳,按照我們上面的名詞解釋课幕,算是Stub的一種用法,只是用Mock類來實(shí)現(xiàn)的五垮。

2). 使用side_effect乍惊,依次返回指定值:

class PersonTest(unittest.TestCase):
    def test_should_get_age(self):
        p = Person()
        
        p.get_age = mock.Mock(side_effect=[10, 11, 12])

        self.assertEqual(p.get_age(), 10)
        self.assertEqual(p.get_age(), 11)
        self.assertEqual(p.get_age(), 12)

get_page()每一次被調(diào)用的時(shí)候都會(huì)到Mock的side_effect中去取一個(gè)值。如果調(diào)用次數(shù)超過了side_effect中的個(gè)數(shù)放仗,程序運(yùn)行時(shí)會(huì)報(bào)錯(cuò)StopIteration润绎。

3). 打算輸出為異常時(shí):

p.get_age = mock.Mock(return_value =30,side_effect=Exception('Boom!'))

self.assertRaises(TypeError,p.get_age)

只要調(diào)用就會(huì)拋出異常。

  1. 檢驗(yàn)是否調(diào)用
    def test_should_validate_method_calling(self):
            p = Person()

            p.get_fullname = mock.Mock(return_value='Tracy cheng')

            # 沒調(diào)用過
            p.get_fullname.assert_not_called()  # Python 3.5

            p.get_fullname('1', '2')

            # # 調(diào)用過任意次數(shù)
            # p.get_fullname.assert_called()  # Python 3.6
            # # 只調(diào)用過一次, 不管參數(shù)
            # p.get_fullname.assert_called_once()  # Python 3.6
            # 只調(diào)用過一次,并且符合指定的參數(shù)
            p.get_fullname.assert_called_once_with('1', '2')

            p.get_fullname('3', '4')
            # 只要調(diào)用過即可莉撇,必須指定參數(shù)
            p.get_fullname.assert_any_call('1', '2')

            # 重置mock呢蛤,重置之后相當(dāng)于沒有調(diào)用過
            p.get_fullname.reset_mock()
            p.get_fullname.assert_not_called()

            # Mock對(duì)象里除了return_value, side_effect屬性外,
            # called表示是否調(diào)用過棍郎,call_count可以返回調(diào)用的次數(shù)
            self.assertEqual(p.get_fullname.called, False)
            self.assertEqual(p.get_fullname.call_count, 0)

            p.get_fullname('1', '2')
            p.get_fullname('3', '4')
            self.assertEqual(p.get_fullname.called, True)
            self.assertEqual(p.get_fullname.call_count, 2)

其中的assert_called和assert_called_once是python3.6中的用法其障,注意一下Python的版本。


稍微高階一丟丟的用法:
靜態(tài)方法和模塊方法需要用到Patch來mock涂佃。其中會(huì)用到Patch裝修器荧琼,包含有: patch(), patch.object() and patch.dict().

patch和patch.object這兩個(gè)函數(shù)都會(huì)返回一個(gè)mock內(nèi)部的類實(shí)例尝哆,這個(gè)類是class _patch。返回的這個(gè)類實(shí)例既可以作為函數(shù)的裝飾器,也可以作為類的裝飾器苟翻,也可以作為上下文管理器伦泥。使用patch或者patch.object的目的是為了控制mock的范圍柜候,意思就是在一個(gè)函數(shù)范圍內(nèi)明场,或者一個(gè)類的范圍內(nèi),或者with語句的范圍內(nèi)mock掉一個(gè)對(duì)象午笛。

# 在patch中給出定義好的Mock的對(duì)象惭蟋,好處是定義好的對(duì)象可以復(fù)用

    def test_should_get_class_name(self):
        mock_get_class_name = mock.Mock(return_value='Man')
        with mock.patch.object(Person,'get_class_name',mock_get_class_name):
            self.assertEqual('Man',Person.get_class_name())

當(dāng)你知道了mock能做什么之后,要如何學(xué)習(xí)并掌握mock呢季研?最好的方式就是查看閱讀官方文檔敞葛,并在自己的單元測(cè)試中使用。

也有一些大神已經(jīng)封裝出更好使用的第三方Python Mock庫与涡,可參見:
Python中好用的第三方mock庫-httmock

拓展:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末惹谐,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子驼卖,更是在濱河造成了極大的恐慌氨肌,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酌畜,死亡現(xiàn)場(chǎng)離奇詭異怎囚,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)桥胞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門恳守,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贩虾,你說我怎么就攤上這事催烘。” “怎么了缎罢?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵伊群,是天一觀的道長(zhǎng)考杉。 經(jīng)常有香客問我,道長(zhǎng)舰始,這世上最難降的妖魔是什么崇棠? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮丸卷,結(jié)果婚禮上枕稀,老公的妹妹穿的比我還像新娘。我一直安慰自己谜嫉,他們只是感情好抽莱,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著骄恶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪匕垫。 梳的紋絲不亂的頭發(fā)上僧鲁,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音象泵,去河邊找鬼寞秃。 笑死,一個(gè)胖子當(dāng)著我的面吹牛偶惠,可吹牛的內(nèi)容都是我干的春寿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼忽孽,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼绑改!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起兄一,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤厘线,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后出革,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體造壮,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年骂束,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了耳璧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡展箱,死狀恐怖旨枯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情析藕,我是刑警寧澤召廷,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布凳厢,位于F島的核電站,受9級(jí)特大地震影響竞慢,放射性物質(zhì)發(fā)生泄漏先紫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一筹煮、第九天 我趴在偏房一處隱蔽的房頂上張望遮精。 院中可真熱鬧,春花似錦败潦、人聲如沸本冲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽檬洞。三九已至,卻和暖如春沟饥,著一層夾襖步出監(jiān)牢的瞬間添怔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工贤旷, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留广料,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓幼驶,卻偏偏與公主長(zhǎng)得像艾杏,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子盅藻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容