Python接口自動(dòng)化之mock模塊簡(jiǎn)單使用

mock簡(jiǎn)介

? ? ? ? mock原是python的第三方庫(kù),python 2可以直接安裝mock模塊,但在python 3.3以后mock模塊已經(jīng)整合到了unittest測(cè)試框架中,不需要再單獨(dú)安裝。

????????Mock這個(gè)詞在英語(yǔ)中有模擬的這個(gè)意思,因此我們可以猜測(cè)出這個(gè)庫(kù)的主要功能是模擬一些東西。準(zhǔn)確的說(shuō),Mock是Python中一個(gè)用于支持單元測(cè)試的庫(kù),它的主要功能是使用mock對(duì)象替代掉指定的Python對(duì)象,以達(dá)到模擬對(duì)象的行為。簡(jiǎn)單的說(shuō),mock庫(kù)用于如下的場(chǎng)景:

假設(shè)你開發(fā)的一個(gè)api在工作的時(shí)候需要調(diào)用發(fā)送請(qǐng)求給特定的服務(wù)器來(lái)得到一個(gè)JSON返回值,然后根據(jù)這個(gè)返回值來(lái)做處理。如果要為該API寫一個(gè)單元測(cè)試,該如何做?

一個(gè)簡(jiǎn)單的辦法是搭建一個(gè)測(cè)試的服務(wù)器,在單元測(cè)試的時(shí)候,讓該API和這個(gè)測(cè)試服務(wù)器交互。但是這種做法有兩個(gè)問(wèn)題:

1、測(cè)試服務(wù)器可能很不好搭建,或者搭建效率很低。

2、你搭建的測(cè)試服務(wù)器可能無(wú)法返回所有可能的值,或者需要大量的工作才能達(dá)到這個(gè)目的。

那么如何在沒有測(cè)試服務(wù)器的情況下進(jìn)行上面這種情況的單元測(cè)試呢?Mock模塊就是答案。因?yàn)閙ock模塊可以替換Python對(duì)象,返回值能夠由我們的mock對(duì)象來(lái)決定,而不需要服務(wù)器的參與




mock作用:

1. 解決依賴問(wèn)題:當(dāng)我們測(cè)試一個(gè)接口或者功能模塊的時(shí)候,如果這個(gè)接口或者功能模塊依賴其他接口或其他模塊,那么如果所依賴的接口或功能模塊未開發(fā)完畢,那么我們就可以使用mock模擬被依賴接口,完成目標(biāo)接口的測(cè)試

2. 單元測(cè)試:如果某個(gè)功能未開發(fā)完成,我們又要進(jìn)行測(cè)試用例的代碼編寫,我們也可以先模擬這個(gè)功能進(jìn)行測(cè)試

3. 模擬復(fù)雜業(yè)務(wù)的接口:實(shí)際工作中如果我們?cè)跍y(cè)試一個(gè)接口功能時(shí),如果這個(gè)接口依賴一個(gè)非常復(fù)雜的接口業(yè)務(wù),那么我們完全可以使用mock來(lái)模擬這個(gè)復(fù)雜的業(yè)務(wù)接口,其實(shí)這個(gè)和解決接口依賴是一樣的原理

4.前后端聯(lián)調(diào):如果你是一個(gè)前端頁(yè)面開發(fā),現(xiàn)在需要開發(fā)一個(gè)功能:根據(jù)后臺(tái)返回的狀態(tài)展示不同的頁(yè)面,那么你就需要調(diào)用后臺(tái)的接口,但是后臺(tái)接口還未開發(fā)完成,是不是你就停止這部分工作呢?答案是否定的,你完全可以借助mock來(lái)模擬后臺(tái)這個(gè)接口返回你想要的數(shù)據(jù)

Mock的安裝和導(dǎo)入

1、在Python 3.3以前的版本中,需要另外安裝mock模塊,可以使用pip命令來(lái)安裝:

pip install mock

然后在代碼中就可以直接import進(jìn)來(lái):

import mock


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

from unittest import mock



Mock對(duì)象

????Mock對(duì)象是mock模塊中最重要的概念。Mock對(duì)象就是mock模塊中的一個(gè)類的實(shí)例,這個(gè)類的實(shí)例可以用來(lái)替換其他的Python對(duì)象,來(lái)達(dá)到模擬的效果。Mock類的定義如下:

classMock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, **kwargs)

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

1、找到你要替換的對(duì)象,這個(gè)對(duì)象可以是一個(gè)類,或者是一個(gè)函數(shù),或者是一個(gè)類實(shí)例。

2、然后實(shí)例化Mock類得到一個(gè)mock對(duì)象,并且設(shè)置這個(gè)mock對(duì)象的行為,比如被調(diào)用的時(shí)候返回什么值,被訪問(wèn)成員的時(shí)候返回什么值等。

3、使用這個(gè)mock對(duì)象替換掉我們想替換的對(duì)象,也就是步驟1中確定的對(duì)象。

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


mock實(shí)例及基本用法

????一個(gè)未開發(fā)完成的功能如何測(cè)試?假如們現(xiàn)在有一個(gè)實(shí)現(xiàn)兩個(gè)數(shù)相加的功能需要編寫測(cè)試用例,但是由于開發(fā)進(jìn)度緩慢,只搭兩個(gè)簡(jiǎn)單的框架,并沒有內(nèi)部實(shí)現(xiàn)。

import unittest

from unittest import mock

class SubClass(object):

? ? def add(self, a, b):

? ? ? ? """兩個(gè)數(shù)相加"""

? ? ? ? pass

class TestSub(unittest.TestCase):

  """測(cè)試兩個(gè)數(shù)相加用例"""

? ? def test_sub(self):

? ? ? ? sub = SubClass()? # 初始化被測(cè)函數(shù)類實(shí)例

? ? ? ? sub.add = mock.Mock(return_value=10)? # mock add方法 返回10

? ? ? ? result = sub.add(5, 5)? # 調(diào)用被測(cè)函數(shù)

? ? ? ? self.assertEqual(result, 10)? # 斷言實(shí)際結(jié)果和預(yù)期結(jié)果

if __name__ == '__main__':

? ? unittest.main()

測(cè)試結(jié)果:

----------------------------------------------------------------------

Ran 1 test in 0.000s

OK

Process finished with exit code?

測(cè)試結(jié)果顯示,測(cè)試用例執(zhí)行通過(guò)了。

實(shí)際上mock模擬add方法的原理是 使用相同的對(duì)象方法接收mock的對(duì)象(使用sub.add接收),那么當(dāng)mock對(duì)象被調(diào)用時(shí)(sub.add())就會(huì)返回return_value參數(shù)對(duì)應(yīng)的數(shù)據(jù)




稍微高級(jí)的用法

1、class Mock的參數(shù)

上面講的是mock對(duì)象最基本的用法。下面來(lái)看看mock對(duì)象的稍微高級(jí)點(diǎn)的用法

先來(lái)看看Mock這個(gè)類的參數(shù),在上面看到的類定義中,我們知道它有好幾個(gè)參數(shù),這里介紹最主要的幾個(gè):

name: 這個(gè)是用來(lái)命名一個(gè)mock對(duì)象,只是起到標(biāo)識(shí)作用,當(dāng)你print一個(gè)mock對(duì)象的時(shí)候,可以看到它的name。

return_value: 這個(gè)我們剛才使用過(guò)了,這個(gè)字段可以指定一個(gè)值(或者對(duì)象),當(dāng)mock對(duì)象被調(diào)用時(shí),如果side_effect函數(shù)返回的是DEFAULT,則對(duì)mock對(duì)象的調(diào)用會(huì)返回return_value指定的值。

side_effect: 這個(gè)參數(shù)指向一個(gè)可調(diào)用對(duì)象,一般就是函數(shù)。當(dāng)mock對(duì)象被調(diào)用時(shí),如果該函數(shù)返回值不是DEFAULT時(shí),那么以該函數(shù)的返回值作為mock對(duì)象調(diào)用的返回值。

上面的例子,我們把用例的代碼修改一句如下:

class TestSub(unittest.TestCase):

? ? """測(cè)試兩個(gè)數(shù)相加"""

? ? def test_sub(self):

? ? ? ? sub = SubClass()? # 初始化被測(cè)函數(shù)類實(shí)例

? ? ? ? sub.add = mock.Mock(return_value=10, side_effect=sub.add)? # 傳遞side_effect關(guān)鍵字參數(shù), 會(huì)覆蓋return_value參數(shù)值, 使用真實(shí)的add方法測(cè)試

? ? ? ? result = sub.add(5, 11)? # 真正的調(diào)用被測(cè)函數(shù)

? ? ? ? self.assertEqual(result, 16)? # 斷言實(shí)際結(jié)果和預(yù)期結(jié)果

????????代碼中我們給Mock方法添加了另一個(gè)關(guān)鍵字參數(shù)side_effect = sub.add, 這個(gè)參數(shù)和return_value 正好相反,當(dāng)傳遞這個(gè)參數(shù)的時(shí)候return_value 參數(shù)就會(huì)失效

  而side_effect生效,這里我給的參數(shù)值是sub.add 相當(dāng)于add方法的地址,那么當(dāng)調(diào)用add方法時(shí)就會(huì)真實(shí)的使用add方法,也就達(dá)到了我們測(cè)試實(shí)際的add 方法。

  你也可以理解為當(dāng)傳遞了side_effect參數(shù)且值為被測(cè)方法地址時(shí),mock就不會(huì)起作用

  side_effect接收的是一個(gè)可迭代序列,當(dāng)傳遞多個(gè)值時(shí),那么每次調(diào)用mock時(shí)會(huì)返回不同的值

mock_obj = mock.Mock(side_effect= [1,2,3])

print(mock_obj())

print(mock_obj())

print(mock_obj())

print(mock_obj())

輸出

Traceback (most recent call last):

1

? File "D:/MyThreading/mymock.py", line 37, in <module>

2

? ? print(mock_obj())

3

? File "C:\Python36\lib\unittest\mock.py", line 939, in __call__

? ? return _mock_self._mock_call(*args, **kwargs)

? File "C:\Python36\lib\unittest\mock.py", line 998, in _mock_call

? ? result = next(effect)

StopIteration

Process finished with exit code 1

當(dāng)所有值被取完后就會(huì)報(bào)錯(cuò)(這個(gè)地方有點(diǎn)類似生成器的原理)


2、mock對(duì)象的自動(dòng)創(chuàng)建

當(dāng)訪問(wèn)一個(gè)mock對(duì)象中不存在的屬性時(shí),mock會(huì)自動(dòng)建立一個(gè)子mock對(duì)象,并且把正在訪問(wèn)的屬性指向它,這個(gè)功能對(duì)于實(shí)現(xiàn)多級(jí)屬性的mock很方便。

client= mock.Mock()client.v2_client.get.return_value ='200'

這個(gè)時(shí)候,你就得到了一個(gè)mock過(guò)的client實(shí)例,調(diào)用該實(shí)例的v2_client.get()方法會(huì)得到的返回值是"200"。

從上面的例子中還可以看到,指定mock對(duì)象的return_value還可以使用屬性賦值的方法。




存在依賴關(guān)系的功能如何測(cè)試?

? ? ? ?假設(shè)有這樣一個(gè)場(chǎng)景:我們要測(cè)試一個(gè)支付接口但是這個(gè)支付接口又依賴一個(gè)第三方支付接口,那么第三方支付接口我們暫時(shí)沒有權(quán)限使用,那么我們?cè)撊绾螠y(cè)試我們自己這個(gè)接口呢?

  看下面的實(shí)例,假設(shè)第三方接口和我們自己的支付接口如下:

import requests

class PayApi(object):

? ? @staticmethod

? ? def auth(card, amount):

? ? ? ? """

? ? ? ? 第三方支付接口

? ? ? ? :param card: 卡號(hào)

? ? ? ? :param amount: 支付金額

? ? ? ? :return:

? ? ? ? """

? ? ? ? pay_url = "http://www.zhifubao.com"? # 第三方支付接口地址

? ? ? ? data = {"card": card, "amount": amount}

? ? ? ? response = requests.post(pay_url, data=data)? # 請(qǐng)求第三方支付接口

? ? ? ? return response? # 返回狀態(tài)碼

? ? def pay(self, user_id, card, amount):

? ? ? ? """

? ? ? ? 我們自己的支付接口

? ? ? ? :param user_id: 用戶id

? ? ? ? :param card: 卡號(hào)

? ? ? ? :param amount: 支付金額

? ? ? ? :return:

? ? ? ? """

     # 調(diào)用第三方支付接口

? ? ? ? response = self.auth(card, amount)

? ? ? ? try:

? ? ? ? ? ? if response['status_code'] == '200':

? ? ? ? ? ? ? ? print('用戶{}支付金額{}成功'.format(user_id, amount))

? ? ? ? ? ? ? ? return '支付成功'

? ? ? ? ? ? elif response['status_code'] == '500':

? ? ? ? ? ? ? ? print('用戶{}支付失敗, 金額不變'.format(user_id))

? ? ? ? ? ? ? ? return '支付失敗'

? ? ? ? ? ? else:

? ? ? ? ? ? ? ? return '未知錯(cuò)誤'

? ? ? ? except Exception:

? ? ? ? ? ? return "Error, 服務(wù)器異常!"

if __name__ == '__main__':

? ? pass

很明顯第三方支付接口是無(wú)法訪問(wèn)的,因?yàn)榻涌诘牡刂肥俏褼IY的,為了模擬實(shí)際中我們無(wú)法使用的第三方支付接口。編寫測(cè)試用例如下:

import unittest

from unittest import mock

from payment.PayMent import PayApi

class TestPayApi(unittest.TestCase):

? ? def test_success(self):

? ? ? ? pay = PayApi()

? ? ? ? pay.auth = mock.Mock(return_value={'status_code':'200'})

? ? ? ? status = pay.pay('1000', '12345', '10000')

? ? ? ? self.assertEqual(status, '支付成功')

? ? def test_fail(self):

? ? ? ? pay = PayApi()

? ? ? ? pay.auth = mock.Mock(return_value={'status_code':'500'})

? ? ? ? status = pay.pay('1000', '12345', '10000')

? ? ? ? self.assertEqual(status, '支付失敗')

? ? def test_error(self):

? ? ? ? pay = PayApi()

? ? ? ? pay.auth = mock.Mock(return_value={'status_code':'300'})

? ? ? ? status = pay.pay('1000', '12345', '10000')

? ? ? ? self.assertEqual(status, '未知錯(cuò)誤')

? ? def test_exception(self):

? ? ? ? pay = PayApi()

? ? ? ? pay.auth = mock.Mock(return_value='200')

? ? ? ? status = pay.pay('1000', '12345', '10000')

? ? ? ? self.assertEqual(status, 'Error, 服務(wù)器異常!')

if __name__ == '__main__':

? ? unittest.main()

測(cè)試輸出結(jié)果:

....用戶1000支付失敗, 金額不變

用戶1000支付金額10000成功

----------------------------------------------------------------------

Ran 4 tests in 0.001s

OK

Process finished with exit code 0

????從執(zhí)行結(jié)果可以看出,即使第三方支付接口無(wú)法使用,但是我們自己的支付接口仍然測(cè)試通過(guò)了。也許有人會(huì)問(wèn),第三方支付都不能用,我們的測(cè)試結(jié)果是否是有效的呢?通常我們?cè)跍y(cè)試一個(gè)模塊的時(shí)候,我們是可以認(rèn)為其他模塊的功能是正常的,只針對(duì)目標(biāo)模塊進(jìn)行測(cè)試是沒有任何問(wèn)題的,所以說(shuō)測(cè)試結(jié)果也是正確的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評(píng)論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,786評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,438評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,796評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,995評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,230評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,697評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374