Python之學(xué)會(huì)測(cè)試誓禁,讓開(kāi)發(fā)更加高效

? ? ? ? 前幾天,聽(tīng)了公司某位大佬關(guān)于編程心得的體會(huì)肾档,其中講到了“測(cè)試驅(qū)動(dòng)開(kāi)發(fā)”摹恰,感覺(jué)自己的測(cè)試技能薄弱辫继,因此,寫(xiě)下這篇文章俗慈,希望對(duì)測(cè)試能有個(gè)入門(mén)姑宽。這段時(shí)間,筆者也體會(huì)到了測(cè)試的價(jià)值闺阱,一句話(huà)炮车,學(xué)會(huì)測(cè)試,能夠讓你的開(kāi)發(fā)更加高效酣溃。

本文將介紹以下兩個(gè)方面的內(nèi)容:

Test with Coverage

Mock

Test with Coverage

測(cè)試覆蓋率通常被用來(lái)衡量測(cè)試的充分性和完整性瘦穆。從廣義的角度講,主要分為兩大類(lèi):面向項(xiàng)目的需求覆蓋率和更偏向技術(shù)的代碼覆蓋率赊豌。對(duì)于開(kāi)發(fā)人員來(lái)說(shuō)扛或,我們更注重代碼覆蓋率。

代碼覆蓋率指的是至少執(zhí)行了一次的條目數(shù)占整個(gè)條目數(shù)的百分比亿絮。如果條目數(shù)是語(yǔ)句告喊,對(duì)應(yīng)的就是代碼行覆蓋率;如果條目數(shù)是函數(shù)派昧,對(duì)應(yīng)的就是函數(shù)覆蓋率黔姜;如果條目數(shù)是路徑,對(duì)應(yīng)的就是路徑覆蓋率蒂萎,等等秆吵。統(tǒng)計(jì)代碼覆蓋率的根本目的是找出潛在的遺漏測(cè)試用例,并有針對(duì)性的進(jìn)行補(bǔ)充五慈,同時(shí)還可以識(shí)別出代碼中那些由于需求變更等原因造成的廢棄代碼纳寂。通常我們希望代碼覆蓋率越高越好,代碼覆蓋率越高越能說(shuō)明你的測(cè)試用例設(shè)計(jì)是充分且完備的泻拦,但測(cè)試的成本會(huì)隨著代碼覆蓋率的提高而增加毙芜。

在Python中,coverage模塊幫助我們實(shí)現(xiàn)了代碼行覆蓋率争拐,我們可以方便地使用它來(lái)完整測(cè)試的代碼行覆蓋率腋粥。

我們通過(guò)一個(gè)例子來(lái)介紹coverage模塊的使用。

首先架曹,我們有腳本func_add.py隘冲,實(shí)現(xiàn)了add函數(shù),代碼如下:

#?-*-?coding:?utf-8?-*-

defadd(a,?b):

ifisinstance(a,?str)andisinstance(b,?str):

returna?+'+'+?b

elifisinstance(a,?list)andisinstance(b,?list):

returna?+?b

elifisinstance(a,?(int,?float))andisinstance(b,?(int,?float)):

returna?+?b

else:

returnNone

在add函數(shù)中绑雄,分四種情況實(shí)現(xiàn)了加法展辞,分別是字符串,列表万牺,屬性值罗珍,以及其它情況洽腺。

接著,我們用unittest模塊來(lái)進(jìn)行單元測(cè)試靡砌,代碼腳本(test_func_add.py)如下:

importunittest

fromfunc_addimportadd

classTest_Add(unittest.TestCase):

defsetUp(self):

pass

deftest_add_case1(self):

a?="Hello"

b?="World"

res?=?add(a,?b)

print(res)

self.assertEqual(res,"Hello+World")

deftest_add_case2(self):

a?=1

b?=2

res?=?add(a,?b)

print(res)

self.assertEqual(res,3)

deftest_add_case3(self):

a?=?[1,2]

b?=?[3]

res?=?add(a,?b)

print(res)

self.assertEqual(res,?[1,2,3])

deftest_add_case4(self):

a?=2

b?="3"

res?=?add(a,?b)

print(None)

self.assertEqual(res,None)

if__name__?=='__main__':

#?部分用例測(cè)試

#?構(gòu)造一個(gè)容器用來(lái)存放我們的測(cè)試用例

suite?=?unittest.TestSuite()

#?添加類(lèi)中的測(cè)試用例

suite.addTest(Test_Add('test_add_case1'))

suite.addTest(Test_Add('test_add_case2'))

#?suite.addTest(Test_Add('test_add_case3'))

#?suite.addTest(Test_Add('test_add_case4'))

run?=?unittest.TextTestRunner()

run.run(suite)

在這個(gè)測(cè)試中已脓,我們只測(cè)試了前兩個(gè)用例,也就是對(duì)字符串和數(shù)值型的加法進(jìn)行測(cè)試通殃。

在命令行中輸入coverage run test_func_add.py命令運(yùn)行該測(cè)試腳本度液,輸出結(jié)果如下:

Hello+World

.3

.

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

Ran2testsin0.000s

OK

再輸入命令coverage html就能生成代碼行覆蓋率的報(bào)告,會(huì)生成htmlcov文件夾画舌,打開(kāi)其中的index.html文件堕担,就能看到本次執(zhí)行的覆蓋率情況,如下圖:

測(cè)試覆蓋率結(jié)果總覽

我們點(diǎn)擊func_add.py查看add函數(shù)測(cè)試的情況曲聂,如下圖:

func_add.py腳本的測(cè)試覆蓋率情況

可以看到霹购,單元測(cè)試腳本test_func_add.py的前兩個(gè)測(cè)試用例只覆蓋到了add函數(shù)中左邊綠色的部分,而沒(méi)有測(cè)試到紅色的部分朋腋,代碼行覆蓋率為75%齐疙。

??因此,還有兩種情況沒(méi)有覆蓋到旭咽,說(shuō)明我們的單元測(cè)試中的測(cè)試用例還不夠充分贞奋。

??在test_func_add.py中,我們把main函數(shù)中的注釋去掉穷绵,把后兩個(gè)測(cè)試用例也添加進(jìn)來(lái)轿塔,這時(shí)候我們?cè)龠\(yùn)行上面的coverage模塊的命令,重新生成htmlcov后仲墨,func_add.py的代碼行覆蓋率如下圖:

增加測(cè)試用例后,func_add.py腳本的測(cè)試覆蓋率情況

??可以看到目养,增加測(cè)試用例后,我們調(diào)用的add函數(shù)代碼行覆蓋率為100%癌蚁,所有的代碼都覆蓋到了。

Mock

Mock這個(gè)詞在英語(yǔ)中有模擬的這個(gè)意思匈勋,因此我們可以猜測(cè)出這個(gè)庫(kù)的主要功能是模擬一些東西礼旅。準(zhǔn)確的說(shuō)膳叨,Mock是Python中一個(gè)用于支持單元測(cè)試的庫(kù)洽洁,它的主要功能是使用mock對(duì)象替代掉指定的Python對(duì)象菲嘴,以達(dá)到模擬對(duì)象的行為汰翠。在Python3中,mock是輔助單元測(cè)試的一個(gè)模塊昭雌。它允許您用模擬對(duì)象替換您的系統(tǒng)的部分复唤,并對(duì)它們已使用的方式進(jìn)行斷言。

在實(shí)際生產(chǎn)中的項(xiàng)目是非常復(fù)雜的烛卧,對(duì)其進(jìn)行單元測(cè)試的時(shí)候,會(huì)遇到以下問(wèn)題:

接口的依賴(lài)

外部接口調(diào)用

測(cè)試環(huán)境非常復(fù)雜

單元測(cè)試應(yīng)該只針對(duì)當(dāng)前單元進(jìn)行測(cè)試, 所有的內(nèi)部或外部的依賴(lài)應(yīng)該是穩(wěn)定的, 已經(jīng)在別處進(jìn)行測(cè)試過(guò)的总放。使用mock 就可以對(duì)外部依賴(lài)組件實(shí)現(xiàn)進(jìn)行模擬并且替換掉, 從而使得單元測(cè)試將焦點(diǎn)只放在當(dāng)前的單元功能。

我們通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明mock模塊的使用局雄。

首先,我們有腳本mock_multipy.py炬搭,主要實(shí)現(xiàn)的功能是Operator類(lèi)中的multipy函數(shù),在這里我們可以假設(shè)該函數(shù)并沒(méi)有實(shí)現(xiàn)好融虽,只是存在這樣一個(gè)函數(shù),代碼如下:

#?-*-?coding:?utf-8?-*-

#?mock_multipy.py

classOperator():

defmultipy(self,?a,?b):

pass

盡管我們沒(méi)有實(shí)現(xiàn)multipy函數(shù)飘言,但是我們還是想對(duì)這個(gè)函數(shù)的功能進(jìn)行測(cè)試,這時(shí)候我們可以借助mock模塊中的Mock類(lèi)來(lái)實(shí)現(xiàn)谆吴。測(cè)試的腳本(mock_example.py)代碼如下:

#?-*-?coding:?utf-8?-*-

fromunittestimportmock

importunittest

frommock_multipyimportOperator

#?test?Operator?class

classTestCount(unittest.TestCase):

deftest_add(self):

op?=?Operator()

#?利用Mock類(lèi),我們假設(shè)返回的結(jié)果為15

op.multipy?=?mock.Mock(return_value=15)

#?調(diào)用multipy函數(shù)句狼,輸入?yún)?shù)為4,5,實(shí)際并未調(diào)用

result?=?op.multipy(4,5)

#?聲明返回結(jié)果是否為15

self.assertEqual(result,15)

if__name__?=='__main__':

unittest.main()

讓我們對(duì)上述的代碼做一些說(shuō)明热某。

op.multipy?=?mock.Mock(return_value=15)

通過(guò)Mock類(lèi)來(lái)模擬調(diào)用Operator類(lèi)中的multipy()函數(shù)腻菇,return_value 定義了multipy()方法的返回值昔馋。

result?=?op.multipy(4,5)

result值調(diào)用multipy()函數(shù),輸入?yún)?shù)為4,5秘遏,但實(shí)際并未調(diào)用,最后通過(guò)assertEqual()方法斷言邦危,返回的結(jié)果是否是預(yù)期的結(jié)果為15舍扰。輸出的結(jié)果如下:

Ran?1testin0.002s

OK

通過(guò)Mock類(lèi)希坚,我們即使在multipy函數(shù)并未實(shí)現(xiàn)的情況下边苹,仍然能夠通過(guò)想象函數(shù)執(zhí)行的結(jié)果來(lái)進(jìn)行測(cè)試裁僧,這樣如果有后續(xù)的函數(shù)依賴(lài)multipy函數(shù)个束,也并不影響后續(xù)代碼的測(cè)試。

利用Mock模塊中的patch函數(shù)播急,我們可以將上述測(cè)試的腳本代碼簡(jiǎn)化如下:

#?-*-?coding:?utf-8?-*-

importunittest

fromunittest.mockimportpatch

frommock_multipyimportOperator

#?test?Operator?class

classTestCount(unittest.TestCase):

@patch("mock_multipy.Operator.multipy")

deftest_case1(self,?tmp):

tmp.return_value?=15

result?=?Operator().multipy(4,5)

self.assertEqual(15,?result)

if__name__?=='__main__':

unittest.main()

patch()裝飾器可以很容易地模擬類(lèi)或?qū)ο笤谀K測(cè)試售睹。在測(cè)試過(guò)程中,您指定的對(duì)象將被替換為一個(gè)模擬(或其他對(duì)象)昌妹,并在測(cè)試結(jié)束時(shí)還原。

那如果我們后面又實(shí)現(xiàn)了multipy函數(shù)飞崖,是否仍然能夠測(cè)試呢?

修改mock_multipy.py腳本固歪,代碼如下:

#?-*-?coding:?utf-8?-*-

#?mock_multipy.py

classOperator():

defmultipy(self,?a,?b):

returna?*?b

這時(shí)候,我們?cè)龠\(yùn)行mock_example.py腳本牢裳,測(cè)試仍然通過(guò),這是因?yàn)閙ultipy函數(shù)返回的結(jié)果仍然是我們mock后返回的值蒲讯,而并未調(diào)用真正的Operator類(lèi)中的multipy函數(shù)。

我們修改mock_example.py腳本如下:

#?-*-?coding:?utf-8?-*-

fromunittestimportmock

importunittest

frommock_multipyimportOperator

#?test?Operator?class

classTestCount(unittest.TestCase):

deftest_add(self):

op?=?Operator()

#?利用Mock類(lèi)局嘁,添加side_effect參數(shù)

op.multipy?=?mock.Mock(return_value=15,?side_effect=op.multipy)

#?調(diào)用multipy函數(shù),輸入?yún)?shù)為4,5,實(shí)際已調(diào)用

result?=?op.multipy(4,5)

#?聲明返回結(jié)果是否為15

self.assertEqual(result,15)

if__name__?=='__main__':

unittest.main()

side_effect參數(shù)和return_value參數(shù)是相反的悦昵。它給mock分配了可替換的結(jié)果,覆蓋了return_value但指。簡(jiǎn)單的說(shuō),一個(gè)模擬工廠(chǎng)調(diào)用將返回side_effect值枚赡,而不是return_value谓谦。所以贫橙,設(shè)置side_effect參數(shù)為Operator類(lèi)中的multipy函數(shù)反粥,那么return_value的作用失效。

運(yùn)行修改后的測(cè)試腳本才顿,測(cè)試結(jié)果如下:

Ran?1testin0.004s

FAILED?(failures=1)

15?!=?20

Expected?:20

Actual???:15

可以發(fā)現(xiàn),multipy函數(shù)返回的值為20郑气,不等于我們期望的值15,這是side_effect函數(shù)的作用結(jié)果使然尾组,返回的結(jié)果調(diào)用了Operator類(lèi)中的multipy函數(shù),所以返回值為20讳侨。

在self.assertEqual(result, 15)中將15改成20,運(yùn)行測(cè)試結(jié)果如下:

Ran?1testin0.002s

OK

??本次分享到此結(jié)束潮峦,感謝大家的閱讀~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末勇婴,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子耕渴,更是在濱河造成了極大的恐慌,老刑警劉巖萨螺,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異椭盏,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)掏颊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)乌叶,“玉大人,你說(shuō)我怎么就攤上這事准浴。” “怎么了乐横?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)罐农。 經(jīng)常有香客問(wèn)我,道長(zhǎng)涵亏,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任溯乒,我火速辦了婚禮豹爹,結(jié)果婚禮上裆悄,老公的妹妹穿的比我還像新娘臂聋。我一直安慰自己,他們只是感情好孩等,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著肄方,像睡著了一般。 火紅的嫁衣襯著肌膚如雪权她。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天蝴罪,我揣著相機(jī)與錄音,去河邊找鬼要门。 笑死虏肾,一個(gè)胖子當(dāng)著我的面吹牛欢搜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播炒瘟,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼唧领!你這毒婦竟也來(lái)了雌续?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤驯杜,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后鸽心,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡藤肢,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年糯景,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嘁圈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蟀淮。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖怠惶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情策治,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布通惫,位于F島的核電站,受9級(jí)特大地震影響讽膏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一料按、第九天 我趴在偏房一處隱蔽的房頂上張望卓箫。 院中可真熱鬧载矿,春花似錦烹卒、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)藐吮。三九已至,卻和暖如春谣辞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背泥从。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留躯嫉,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓和敬,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親昼弟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355