我在講解 Appium 的時候有一篇文章使用到了unittest單元測試框架,從那篇文章就可以看出來這個框架給測試帶來的便利沈矿。今天做一次比較全面系統(tǒng)的介紹。配以大量的腳本示例馍佑,希望可以幫助到更多的朋友。
單元測試就是用一個函數(shù)(方法)去驗(yàn)證另一個函數(shù)(方法)是否正確毛肋。
unittest單元測試框架主要完成以下三件事:
提供用例組織與執(zhí)行:當(dāng)測試用例只有幾條的時候可以不考慮用例的組織,但是當(dāng)測試用例數(shù)量較多時屋剑,此時就需要考慮用例的規(guī)范與組織問題了润匙。unittest單元測試框架就是用來解決這個問題的。
提供豐富的比較方法:既然是測試唉匾,就有一個預(yù)期結(jié)果和實(shí)際結(jié)果的比較問題孕讳。比較就是通過斷言來實(shí)現(xiàn),unittest單元測試框架提供了豐富的斷言方法巍膘。
提供豐富的日志:每一個失敗用例我們都希望知道失敗的原因厂财,所有用例執(zhí)行結(jié)束我們有希望知道整體執(zhí)行情況,比如總體執(zhí)行時間峡懈,失敗用例數(shù)璃饱,成功用例數(shù)。unittest單元測試框架為我們提供了這些數(shù)據(jù)肪康。
比如我們有一個方法是計算兩數(shù)之和荚恶,我們輸入幾組數(shù)值求和,如果結(jié)果與預(yù)期結(jié)果5相同磷支,那我們就認(rèn)為這個結(jié)果是我們想要的:
import unittest
class Count(object):
def __init__(self,a,b):
self.a = a
self.b = b
# 計算加法
def add(self):
return self.a + self.b
# 單元測試谒撼,測試 add()方法
class TestCount(unittest.TestCase):
def setUp(self):
print("Test Start")
def test_add(self):
# 這里參數(shù)3,4是我們寫死的齐唆,實(shí)際是可能變化不固定的嗤栓。比如將讀取到的表格內(nèi)的數(shù)據(jù)當(dāng)做這兩個參數(shù)。
s = Count(3,4)
# 這里的比較對象5就是我們的預(yù)期結(jié)果箍邮,與之不同即為 Fail茉帅。
self.assertEqual(s.add(),5)
def tearDown(self):
print("Test end")
if __name__ == '__main__':
unittest.main()
這里我們將待測方法和單元測試寫在了一個.py文件里,這樣對于日后想要修改帶來一定的不便,所以我們常把待測方法和單元測試分開寫锭弊。
待測試方法:
calculator.py
import unittest
class Count(object):
def __init__(self,a,b):
self.a = a
self.b = b
# 計算加法
def add(self):
return self.a + self.b
單元測試:
test.py
from calculator import Count
# 單元測試必須要引入unittest模塊
import unittest
# 測試方法必須要繼承自unittest.TestCase
class TestCount(unittest.TestCase):
def setUp(self):
print("Test Start測試開始")
def test_add(self):
s = Count(3,4)
self.assertEqual(s.add(),5)
def tearDown(self):
print("Test end測試結(jié)束")
if __name__ == '__main__':
unittest.main()
注意:count.py 和 test.py 要在同一路徑下才能 import堪澎。
這里要特別說明一點(diǎn),測試方法必須要以“test_”開頭味滞,否則unittest.TestCase不識別樱蛤,無法進(jìn)行驗(yàn)證。如果測試方法沒有以“test_”開頭剑鞍,則會報錯昨凡,比如我們將方法名 test_add改為 testadd,執(zhí)行結(jié)果如下:
補(bǔ)充說明:一個.py文件有兩種使用方式:直接使用和模塊調(diào)用蚁署。上面的calculator.py文件就是被模塊調(diào)用便脊,test.py 就是直接使用。腳本最后的 if 語句
if __name__ == '__main__':
unittest.main()
表示當(dāng)前文件只能直接使用光戈,不能模塊調(diào)用哪痰。在這里把這兩行代碼去掉也不會影響運(yùn)行結(jié)果遂赠。
單元測試的重要概念
1. TestCase
一個TestCase的實(shí)例就是一個測試用例。一個測試用例要包括測試前準(zhǔn)備環(huán)境的搭建(setUp)晌杰,執(zhí)行測試代碼(run)跷睦,以及測試后環(huán)境的還原(tearDown)。一個測試用例是一個完整的測試單元肋演,通過運(yùn)行這個測試單元抑诸,可以對某一個功能進(jìn)行驗(yàn)證。
2. TestSuite
對于某一個功能模塊的驗(yàn)證可能需要多個測試用例爹殊,多個測試用例集合在一起執(zhí)行驗(yàn)證某一個功能哼鬓,這樣就是一個TestSuite。通過addTest()方法將 TestCase 加載到 TestSuite()中边灭。
3. TestRunner
TestRunner可以使用圖形界面异希、文本界面,或返回一個特殊的值等方式來表示測試執(zhí)行的結(jié)果绒瘦。TextTestRunner提供的 run()方法來執(zhí)行 test suite/test case称簿。
4.TestFixture
一個測試用例環(huán)境的搭建和銷毀就是一個 fixture。
下面我們舉例說明惰帽,對上面的 test.py 文件進(jìn)行修改:
在實(shí)際操作這個示例的時候遇到一些問題憨降,晚些時候補(bǔ)充!
斷言方法
方法 | 檢查 |
---|---|
assertEqual(a,b) | a==b |
assertNotEqual(a,b) | a!=b |
assertTrue(x) | bool(x) is True |
assertFalse(x) | bool(x) is False |
assertIs(a,b) | a is b |
assertIsNot(a,b) | a is not b |
assertIn(a,b) | a in b |
assertNotIn(a,b) | a not in b |
舉例說明斷言的用法:
count.py
def is_prime(n):
if n <= 1:
return False
for i in range(2, n):
if n % i == 0:
print("i ====哈啊哈哈哈=== %s,%s" % (i, n))
print(len(range(2, n)))
return False
print("i ====哈啊哈哈wo哈=== %s,%s" % (i, n))
print(len(range(2, n)))
return True
test2.py
from count import is_prime
import unittest
class Test(unittest.TestCase):
def setUp(self):
print("測試開始")
def tearDown(self):
print("測試結(jié)束")
def test_case(self):
self.assertTrue(is_prime(9),msg="is Not Prime")
if __name__ == "__main__":
unittest.main()
組織單元測試用例
當(dāng)我們新增被測功能和相應(yīng)的測試用例后该酗,再來看看 unittest 是如何擴(kuò)展和組織新增的測試用例的授药。
我們現(xiàn)在對calculator.py文件新增一個函數(shù):
calculator.py
import unittest
class Count(object):
def __init__(self,a,b):
self.a = a
self.b = b
# 計算加法
def add(self):
return self.a + self.b
# 計算減法
def sub(self):
return self.a - self.b
接下來再修改test.py文件:
from calculator import Count
import unittest
class TestCount(unittest.TestCase):
def setUp(self):
print("測試開始")
def tearDown(self):
print("測試結(jié)束")
def test_add(self):
s = Count(2,1)
self.assertEqual(s.add(), 3)
print("用例1")
def test_add2(self):
s = Count(2,3)
self.assertEqual(s.add(), 5)
print("用例2")
class TestSub(unittest.TestCase):
def setUp(self):
print("測試減法開始")
def tearDown(self):
print("測試減法結(jié)束")
def test_sub(self):
s = Count(4, 1)
self.assertEqual(s.sub(), 3, msg="4 - 1 != 2")
def test_sub2(self):
s = Count(3, 1)
self.assertEqual(s.sub(), 2)
if __name__ == '__main__':
print("到這了")
# 構(gòu)造測試集
suite = unittest.TestSuite()
suite.addTest(TestCount.test_add())
suite.addTest(TestCount.test_add2())
suite.addTest(TestSub.test_sub())
suite.addTest(TestSub.test_sub2())
# 執(zhí)行測試
runner = unittest.TextTestRunner()
runner.run(suite)
這個腳本執(zhí)行的結(jié)果是四個函數(shù)全都 pass:
現(xiàn)在我們對測試腳本稍加修改,使得結(jié)果有成功有失斘仄恰:
test.py
from calculator import Count
import unittest
class TestCount(unittest.TestCase):
def setUp(self):
print("測試開始")
def tearDown(self):
print("測試結(jié)束")
def test_add(self):
s = Count(2,1)
self.assertEqual(s.add(), 3)
print("用例1")
def test_add2(self):
s = Count(2,3)
self.assertEqual(s.add(), 15)
print("用例2")
class TestSub(unittest.TestCase):
def setUp(self):
print("測試減法開始")
def tearDown(self):
print("測試減法結(jié)束")
def test_sub(self):
s = Count(4, 1)
self.assertEqual(s.sub(), 2, msg="錯誤原因:4 - 1 != 2")
def test_sub2(self):
s = Count(3, 1)
self.assertEqual(s.sub(), 2)
if __name__ == '__main__':
print("到這了")
# 構(gòu)造測試集
suite = unittest.TestSuite()
suite.addTest(TestCount.test_add())
suite.addTest(TestCount.test_add2())
suite.addTest(TestSub.test_sub())
suite.addTest(TestSub.test_sub2())
# 執(zhí)行測試
runner = unittest.TextTestRunner()
runner.run(suite)
執(zhí)行結(jié)果如下:
從結(jié)果我們能夠看出來悔叽,一共4個用例,其中2個測試通過爵嗅,2個未能通過娇澎,未通過的函數(shù)和原因都已經(jīng)列出。
通過觀察上面的腳本還是有重復(fù)代碼睹晒,我們可以繼續(xù)修改:
from calculator import Count
import unittest
class MyTest(unittest.TestCase):
def setUp(self):
print("測試開始")
def tearDown(self):
print("測試結(jié)束")
class TestCount(MyTest):
def test_add(self):
s = Count(2,1)
self.assertEqual(s.add(), 3)
print("用例1")
def test_add2(self):
s = Count(2,3)
self.assertEqual(s.add(), 5)
print("用例2")
class TestSub(MyTest):
def setUp(self):
print("測試減法開始")
def test_sub(self):
s = Count(4, 1)
self.assertEqual(s.sub(), 3, msg="錯誤原因:4 - 1 != 2")
def test_sub2(self):
s = Count(3, 1)
self.assertEqual(s.sub(), 2)
if __name__ == '__main__':
print("到這了")
# 構(gòu)造測試集
suite = unittest.TestSuite()
suite.addTest(TestCount.test_add())
suite.addTest(TestCount.test_add2())
suite.addTest(TestSub.test_sub())
suite.addTest(TestSub.test_sub2())
# 執(zhí)行測試
runner = unittest.TextTestRunner()
runner.run(suite)
上面腳本類test_add和TestSub都繼承 MyTest趟庄,而 MyTest 又繼承unittest.TestCase,所以這兩個類也就繼承了unittest.TestCase伪很。這樣封裝的前提是戚啥,這兩個類都必須在 setUp 和 tearDown 中干的事情是一樣的。
我在寫這些腳本的時候曾思考過一個問題锉试。就拿我們公司目前已有的測試用例(已經(jīng)有接近400個)來說猫十,如果都寫在 test.py 一個文件里,那這個文件該有多冗余,日后維護(hù)起來該何等麻煩炫彩,有沒有一個更好的辦法來組織這些測試用例呢?別說絮短,還真有江兢!其實(shí)接下來要介紹的內(nèi)容在我介紹 Appium 的時候已經(jīng)使用該方法組織用例了。
首先我們分析一下上面的 test.py 文件不好在哪里丁频。add()和 sub()兩個方法分別實(shí)現(xiàn)兩個不同的功能杉允,為了驗(yàn)證這兩個方法,那就需要兩個類來實(shí)現(xiàn)席里,如果有更多的功能需要驗(yàn)證叔磷,那就需要更多的類,所以我們把這些類都拆分開奖磁。每一個函數(shù)(方法)作為一個單元測試文件改基。
testadd.py
from calculator import Count
import unittest
class TestAdd(unittest.TestCase):
def setUp(self):
print()
def tearDown(self):
print()
def test_add(self):
# Count類有兩個參數(shù),創(chuàng)建對象的時候要有兩個參數(shù)咖为。
s = Count(2,4)
self.assertEqual(s.add(), 6, msg="實(shí)際結(jié)果和預(yù)期不符")
def test_add2(self):
s = Count(1, 4)
self.assertEqual(s.add(), 5)
if __name__ == '_main__':
unittest.main()
testsub.py
from calculator import Count
import unittest
class TestSub(unittest.TestCase):
def setUp(self):
print()
def tearDown(self):
print()
def test_sub(self):
# Count類有兩個參數(shù)秕狰,創(chuàng)建對象的時候要有兩個參數(shù)。
s = Count(10,4)
self.assertEqual(s.sub(), 5, msg="實(shí)際結(jié)果和預(yù)期不符")
def test_sub2(self):
s = Count(19, 14)
self.assertEqual(s.sub(), 5)
if __name__ == '_main__':
unittest.main()
如果只是這樣做改變躁染,那么我們驗(yàn)證如果需要驗(yàn)證 add()和 sub()兩個方法鸣哀,就需要分兩次執(zhí)行 testadd.py和 testsub.py 兩個文件。現(xiàn)在我們繼續(xù)組織這兩個腳本吞彤,使得可以一次執(zhí)行這兩個文件我衬。
test.py
import unittest
# 導(dǎo)入測試文件
import testadd, testsub
# 構(gòu)造測試集
suite = unittest.TestSuite()
suite.addTest(testadd.TestAdd("test_add"))
suite.addTest(testadd.TestAdd("test_add2"))
suite.addTest(testsub.TestSub("test_sub"))
suite.addTest(testsub.TestSub("test_sub2"))
if __name__ == '__main__':
# 執(zhí)行測試
runner = unittest.TextTestRunner()
runner.run(suite)
上面這種組織用例的方法要比之前簡潔一些,但是當(dāng)用例更多的時候饰恕,我們還需要通過 addTest()方法將新增的用例添加到 test.py 文件中挠羔。我們講解一個可以自動添加的方法。這就是 TestLoader 類中提供的一個 discover()方法埋嵌。
TestLoader 負(fù)責(zé)根據(jù)各種標(biāo)準(zhǔn)加載測試用例褥赊,并將他們返回給測試套件。正常情況下莉恼,不需要創(chuàng)建這個類的實(shí)例拌喉。unittest 提供了可以共享的 defaultTestLoader 類,可以使用其子類和方法創(chuàng)建實(shí)例俐银,discover()方法就是這個類中的一個方法之一尿背。
test.py
import unittest
test_dir = './'
discover = unittest.defaultTestLoader.discover(test_dir,pattern='test*.py')
if __name__ == '__main__':
# 執(zhí)行測試
runner = unittest.TextTestRunner()
runner.run(discover)
這次的test.py可能就是 終極組織用例的方法了。現(xiàn)在我們介紹一下 discover()方法中參數(shù)的意思:
test_dir:需要加載的單元測試文件的路徑捶惜。因?yàn)槲疫@里 test.py文件和和各個測試用例在同一路徑下田藐,所以
test_dir = './'
。如果不是在同一路徑下,就填寫絕對路徑汽久,比如我的路徑就應(yīng)該是test_dir = /Users/guxuecheng/Desktop/unittest
腳本patten 是一個正則表達(dá)式鹤竭,
pattern='test*.py'
是指加載所有 test 開頭的.py 文件,*表示任意多個字符景醇。
discover()方法會自動的根據(jù)測試目錄(test_dir)匹配查找測試用例文件(test*.py)臀稚,并將查找到的測試用例組裝到測試套件 suite 中,因此可以通過run()方法執(zhí)行 discover三痰。
粗暴的解釋一下最后一段代碼的意思:
通俗的理解name == 'main':假如你叫小明.py吧寺,在朋友眼中,你是小明(name == '小明')散劫;在你自己眼中稚机,你是你自己(name == 'main')。
if name == 'main'的意思是:當(dāng).py文件被直接運(yùn)行時获搏,if name == 'main'之下的代碼塊將被運(yùn)行赖条;當(dāng).py文件以模塊形式被導(dǎo)入時,if name == 'main'之下的代碼塊不被運(yùn)行常熙。
??
在網(wǎng)上看到了一個博主的文章谋币,給我提供了一個新的思路,感覺很不錯症概,在此感謝博主灰藍(lán)蕾额,將文章搬到這篇文章里,方便日后翻閱:
先準(zhǔn)備一些待測方法彼城,這些方法沒有組織到一個類里诅蝶,也沒有初始化參數(shù),很簡練:
mathfunc.py
def add(a, b):
return a+b
def minus(a, b):
return a-b
def multi(a, b):
return a*b
def divide(a, b):
return a/b
接下來測試這些方法:
test_mathfunc.py
# -*- coding: utf-8 -*-
import unittest
from mathfunc import *
class TestMathFunc(unittest.TestCase):
"""Test mathfuc.py"""
def test_add(self):
"""Test method add(a, b)"""
self.assertEqual(3, add(1, 2))
self.assertNotEqual(3, add(2, 2))
def test_minus(self):
"""Test method minus(a, b)"""
self.assertEqual(1, minus(3, 2))
def test_multi(self):
"""Test method multi(a, b)"""
self.assertEqual(6, multi(2, 3))
def test_divide(self):
"""Test method divide(a, b)"""
self.assertEqual(2, divide(6, 3))
self.assertEqual(2.5, divide(5, 2))
if __name__ == '__main__':
unittest.main()
組織 TestSuite
上面的代碼示例了如何編寫一個簡單的測試募壕,但有兩個問題调炬,我們怎么控制用例執(zhí)行的順序呢?(這里的示例中的幾個測試方法并沒有一定關(guān)系舱馅,但之后你寫的用例可能會有先后關(guān)系缰泡,需要先執(zhí)行方法A,再執(zhí)行方法B)代嗤,我們就要用到TestSuite了棘钞。我們添加到TestSuite中的case是會按照添加的順序執(zhí)行的。
問題二是我們現(xiàn)在只有一個測試文件干毅,我們直接執(zhí)行該文件即可宜猜,但如果有多個測試文件,怎么進(jìn)行組織硝逢,總不能一個個文件執(zhí)行吧姨拥,答案也在TestSuite中绅喉。
下面來個例子:
在文件夾中我們再新建一個文件,test_suite.py:
# -*- coding: utf-8 -*-
import unittest
from test_mathfunc import TestMathFunc
if __name__ == '__main__':
suite = unittest.TestSuite()
tests = [TestMathFunc("test_add"), TestMathFunc("test_minus"), TestMathFunc("test_divide")]
suite.addTests(tests)
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
將結(jié)果輸出到文件中
用例組織好了叫乌,但結(jié)果只能輸出到控制臺柴罐,這樣沒有辦法查看之前的執(zhí)行記錄,我們想將結(jié)果輸出到文件憨奸。很簡單革屠,看示例:
修改test_suite.py:
# -*- coding: utf-8 -*-
import unittest
from test_mathfunc import TestMathFunc
if __name__ == '__main__':
suite = unittest.TestSuite()
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))
with open('UnittestTextReport.txt', 'a') as f:
runner = unittest.TextTestRunner(stream=f, verbosity=2)
runner.run(suite)
執(zhí)行此文件,可以看到膀藐,在同目錄下生成了UnittestTextReport.txt,所有的執(zhí)行報告均輸出到了此文件中红省,這下我們便有了txt格式的測試報告了额各。
進(jìn)階——用HTMLTestRunner輸出漂亮的HTML報告
我們能夠輸出txt格式的文本執(zhí)行報告了,但是文本報告太過簡陋吧恃,是不是想要更加高大上的HTML報告虾啦?但unittest自己可沒有帶HTML報告,我們只能求助于外部的庫了痕寓。
官方原版:http://tungwaiyip.info/software/HTMLTestRunner.html
我修改后的: https://pan.baidu.com/s/1kV64YZ9
修改我們的 test_suite.py:
# -*- coding: utf-8 -*-
import unittest
from test_mathfunc import TestMathFunc
from HtmlTestRunner import HTMLTestRunner
if __name__ == '__main__':
suite = unittest.TestSuite()
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))
with open('HTMLReport.html','w') as f:
runner = HTMLTestRunner(stream=f,
report_title='Test Report',
descriptions='Report Details',
verbosity=2,
output='report',
)
runner.run(suite)
總結(jié)一下:
- unittest是Python自帶的單元測試框架傲醉,我們可以用其來作為我們自動化測試框架的用例組織執(zhí)行框架。
- unittest的流程:寫好TestCase呻率,然后由TestLoader加載TestCase到TestSuite硬毕,然后由TextTestRunner來運(yùn)行TestSuite,運(yùn)行的結(jié)果保存在TextTestResult中礼仗,我們通過命令行或者unittest.main()執(zhí)行時吐咳,main會調(diào)用TextTestRunner中的run來執(zhí)行,或者我們可以直接通過TextTestRunner來執(zhí)行用例元践。
- 一個class繼承unittest.TestCase即是一個TestCase韭脊,其中以 test 開頭的方法在load時被加載為一個真正的TestCase。
- verbosity參數(shù)可以控制執(zhí)行結(jié)果的輸出单旁,0 是簡單報告沪羔、1 是一般報告、2 是詳細(xì)報告象浑。
- 可以通過addTest和addTests向suite中添加case或suite蔫饰,可以用TestLoader的loadTestsFrom__()方法。
- 用 setUp()愉豺、tearDown()死嗦、setUpClass()以及 tearDownClass()可以在用例執(zhí)行前布置環(huán)境,以及在用例執(zhí)行后清理環(huán)境
- 我們可以通過skip粒氧,skipIf越除,skipUnless裝飾器跳過某個case,或者用TestCase.skipTest方法。
- 參數(shù)中加stream摘盆,可以將報告輸出到文件:可以用TextTestRunner輸出txt報告翼雀,以及可以用HTMLTestRunner輸出html報告。