? ? ? ? 前幾天,聽(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é)束潮峦,感謝大家的閱讀~