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é)果也是正確的。