1.認(rèn)識(shí)unittest
在 Python 中有諸多單元測(cè)試框架,如 doctest、unittest、pytest、nose 等,Python 2.1 及
其以后的版本已經(jīng)將 unittest 作為一個(gè)標(biāo)準(zhǔn)模塊放入 Python 開發(fā)包中。
2.認(rèn)識(shí)單元測(cè)試
不用單元測(cè)試框架能寫單元測(cè)試嗎?答案是肯定的。單元測(cè)試本質(zhì)上就是通過一段代
碼去驗(yàn)證另外一段代碼,所以不用單元測(cè)試框架也可以寫單元測(cè)試。下面就通過例子演示。
創(chuàng)建一個(gè)被測(cè)試文件 calculator.py。
# _*_ coding:utf-8 _*_
"""
name:zhangxingzai
date:2023/2/22
"""
# 創(chuàng)建一個(gè)計(jì)算器類
class Calculator:
""" 用于完成兩個(gè)數(shù)的加、減、乘、除 """
def __init__(self, a, b):
self.a = int(a)
self.b = int(b)
# 加法
def add(self):
return self.a + self.b
# 減法
def sub(self):
return self.a - self.b
# 乘法
def mul(self):
return self.a * self.b
# 除法
def div(self):
return self.a / self.b
程序非常簡(jiǎn)單,創(chuàng)建一個(gè) Calculator 類,通過__init__()方法接收兩個(gè)參數(shù),并做 int 類
型轉(zhuǎn)換。創(chuàng)建 add()、sub()、mul()、div()方法分別進(jìn)行加、減、乘、除運(yùn)算。
根據(jù)上面實(shí)現(xiàn)的功能,創(chuàng)建 test_calculator.py 文件。
# _*_ coding:utf-8 _*_
"""
name:zhangxingzai
date:2023/2/22
"""
from calculator import Calculator
def test_add():
c = Calculator(3, 6)
result = c.add()
assert result == 9, '加法運(yùn)算失敗!'
def test_sub():
c = Calculator(7, 2)
result = c.sub()
assert result == 5, '減法運(yùn)算失敗!'
def test_mul():
c = Calculator(3, 3)
result = c.mul()
assert result == 10, '乘法運(yùn)算失敗!'
def test_div():
c = Calculator(6, 2)
result = c.div()
assert result == 3, '除法運(yùn)算失敗!'
if __name__ == '__main__':
test_add()
test_sub()
test_mul()
test_div()
運(yùn)行結(jié)果如下:

在測(cè)試代碼中,首先引入 calculator 文件中的 Calculator 類,并對(duì)測(cè)試數(shù)據(jù)進(jìn)行初始化。
接下來調(diào)用該類下面的方法,得到計(jì)算結(jié)果,并斷言結(jié)果是否正確。
這樣的測(cè)試存在著一些問題。首先,我們需要自己定義斷言失敗的提示;其次,當(dāng)一
個(gè)測(cè)試函數(shù)運(yùn)行失敗后,后面的測(cè)試函數(shù)將不再執(zhí)行;最后,執(zhí)行結(jié)果無法統(tǒng)計(jì)。
當(dāng)然,我們可以通過編寫更多的代碼來解決這些問題,但這就偏離了我們做單元測(cè)試
的初衷。我們應(yīng)該將重點(diǎn)放在測(cè)試本身,而不是其他上面。引入單元測(cè)試框架可以很好地
解決這些問題。
下面通過 unittest 單元測(cè)試框架重新編寫測(cè)試用例:
import unittest
from calculator import Calculator
class TestCalculator(unittest.TestCase):
def test_add(self):
c = Calculator(3, 6)
result = c.add()
self.assertEqual(result, 9)
def test_sub(self):
c = Calculator(7, 2)
result = c.sub()
self.assertEqual(result, 5)
def test_mul(self):
c = Calculator(3, 3)
result = c.mul()
self.assertEqual(result, 10)
def test_div(self):
c = Calculator(6, 2)
result = c.div()
self.assertEqual(result, 3)
if __name__ == '__main__':
unittest.main()
運(yùn)行結(jié)果如下:

引入 unittest 模塊。如果想用 unittest 編寫測(cè)試用例,那么一定要遵守它的“規(guī)則”。
(1)創(chuàng)建一個(gè)測(cè)試類,這里為 TestCalculator 類,必須要繼承 unittest 模塊的 TestCase
類。
(2)創(chuàng)建一個(gè)測(cè)試方法,該方法必須以“test”開頭。
接下來的測(cè)試步驟與前面測(cè)試代碼相同。
首先,調(diào)用被測(cè)試類,傳入初始化數(shù)據(jù)。
其次,調(diào)用被測(cè)試方法,得到計(jì)算結(jié)果。通過 unittest 提供的 assertEqual()方法來斷言
結(jié)果是否與預(yù)期結(jié)果相同。該方法由 TestCase 父類提供,由于繼承了該類,所以可以通過
self 調(diào)用。
最后,調(diào)用 unittest 的 main()來執(zhí)行測(cè)試用例,它會(huì)按照前面的兩條規(guī)則查找測(cè)試用例
并執(zhí)行。
3.unittest重要的概念
unittest 文檔中有四個(gè)重要的概念:Test Case、Test Suite、Test Runner 和 Test Fixture。只有理解了這幾個(gè)概念,才能理解單元測(cè)試的基本特征。
1.Test?Case
Test Case 是最小的測(cè)試單元,用于檢查特定輸入集合的特定返回值。unittest 提供了
TestCase 基類,我們創(chuàng)建的測(cè)試類需要繼承該基類,它可以用來創(chuàng)建新的測(cè)試用例。
2.Test Suite
測(cè)試套件是測(cè)試用例、測(cè)試套件或兩者的集合,用于組裝一組要運(yùn)行的測(cè)試。unittest
提供了 TestSuite 類來創(chuàng)建測(cè)試套件。
3.Test Runner
Test Runner 是一個(gè)組件,用于協(xié)調(diào)測(cè)試的執(zhí)行并向用戶提供結(jié)果。Test Runner 可以使
用圖形界面、文本界面或返回特殊值來展示執(zhí)行測(cè)試的結(jié)果。
unittest 提供了 TextTestRunner類運(yùn)行測(cè)試用例,為了生成 HTML 格式的測(cè)試報(bào)告,后面會(huì)選擇使用 HTMLTestRunner 運(yùn)行類。
4.Test Fixture
Test Fixture 代表執(zhí)行一個(gè)或多個(gè)測(cè)試所需的環(huán)境準(zhǔn)備,以及關(guān)聯(lián)的清理動(dòng)作。例如,
創(chuàng)建臨時(shí)或代理數(shù)據(jù)庫、目錄,或啟動(dòng)服務(wù)器進(jìn)程。unittest 中提供了 setUp()/tearDown()、
setUpClass()/tearDownClass()等方法來完成這些操作。
在理解了上面幾個(gè)概念之后,我們對(duì)前面的測(cè)試用例做如下修改:
import unittest
from calculator import Calculator
class TestCalculator(unittest.TestCase):
# 測(cè)試用例前置動(dòng)作
def setUp(self):
print("test start:")
# 測(cè)試用例后置動(dòng)作
def tearDown(self):
print("test end")
def test_add(self):
c = Calculator(3, 5)
result = c.add()
self.assertEqual(result, 8)
def test_sub(self):
c = Calculator(7, 2)
result = c.sub()
self.assertEqual(result, 5)
def test_mul(self):
c = Calculator(3, 3)
result = c.mul()
self.assertEqual(result, 10)
def test_div(self):
c = Calculator(6, 2)
result = c.div()
self.assertEqual(result, 3)
if __name__ == '__main__':
# 創(chuàng)建測(cè)試套件
suit = unittest.TestSuite
suit.addTest(TestCalculator('test_add'))
suit.addTest(TestCalculator("test_sub"))
suit.addTest(TestCalculator("test_mul"))
suit.addTest(TestCalculator("test_div"))
# 創(chuàng)建測(cè)試運(yùn)行器
runner = unittest.TextTestRunner()
runner.run(suit)
首先,創(chuàng)建一個(gè)測(cè)試類并繼承 TestCase 類,在該類下面創(chuàng)建一條以“test”開頭的方法
為測(cè)試用例。這個(gè)前面已有說明,這里再次說明是為了強(qiáng)調(diào)它的重要性。
其次,在測(cè)試類中增加了 setUp()/tearDown()方法,用于定義測(cè)試用例的前置和后置動(dòng)
作。因?yàn)樵诋?dāng)前測(cè)試中暫時(shí)用不上,所以這里定義了一些簡(jiǎn)單的打印。
接下來,是測(cè)試用例的執(zhí)行,這里做了很大的改動(dòng)。首先,拋棄了 unittest 提供的 main()
方法,而是調(diào)用 TestSuite 類下面的 addTest()來添加測(cè)試用例。因?yàn)橐淮沃荒芴砑右粭l用例,
所以需要指定測(cè)試類及測(cè)試方法。然后,再調(diào)用 TextTestRunner 類下面的 run()運(yùn)行測(cè)試套
件
這樣做確實(shí)比直接使用 main()方法要麻煩得多,但也并非沒有優(yōu)點(diǎn)。
首先,測(cè)試用例的執(zhí)行順序可以由測(cè)試套件的添加順序控制,而 main()方法只能按照
測(cè)試類、方法的名稱來執(zhí)行測(cè)試用例。例如,TestA 類比 TestB 類先執(zhí)行,test_add()用例比
test_div()用例先執(zhí)行。
其次,當(dāng)一個(gè)測(cè)試文件中有很多測(cè)試用例時(shí),并不是每次都要執(zhí)行所有的測(cè)試用例,
尤其是比較耗時(shí)的 UI 自動(dòng)化測(cè)試。因而通過測(cè)試套件和測(cè)試運(yùn)行器可以靈活地控制要執(zhí)行
的測(cè)試用例。
執(zhí)行結(jié)果如下:
test start:
test end
test start:
test end
test start:
test end
Ran 4 tests in 0.013s
FAILED (failures=1)
10 != 9
預(yù)期:9
實(shí)際:10
<點(diǎn)擊以查看差異>
Traceback (most recent call last):
File "C:\Users\Administrator\PycharmProjects\AutoFunction\test_calculator.py", line 28, in test_mul
self.assertEqual(result, 10)
AssertionError: 9 != 10
test start:
test end
從執(zhí)行結(jié)果可以看到,setUp/tearDown 作用于每條測(cè)試用例的開始之處與結(jié)束之處。
4.斷言方法
在執(zhí)行測(cè)試用例的過程中,最終測(cè)試用例執(zhí)行成功與否,是通過測(cè)試得到的實(shí)際結(jié)果
與預(yù)期結(jié)果進(jìn)行比較得到的。unittest 框架的 TestCase 類提供的用于測(cè)試結(jié)果的斷言方法如
下表所示。

斷言方法的使用如下所示:
# _*_ coding:utf-8 _*_
import unittest
class TestAssert(unittest.TestCase):
def test_equal(self):
self.assertEqual(2 + 2, 4)
self.assertEqual("python", "python")
self.assertNotEqual("hello", "python")
def test_in(self):
self.assertIn("hello", "hello world")
self.assertNotIn("hi", "hello")
def test_true(self):
self.assertTrue(True)
self.assertFalse(False)
if __name__ == '__main__':
unittest.main()
運(yùn)行上面的測(cè)試用例,即可通過測(cè)試結(jié)果推斷出這些斷言方法是如何使用的。
5.測(cè)試用例的組織與 discover 方法
我們可以在一個(gè)測(cè)試文件中定義多個(gè)測(cè)試類,只要它們遵循測(cè)試用例的“規(guī)則”,main()
方法就可以找到并執(zhí)行它們。但是,我們要測(cè)試的類或方法可能有很多。
下面開發(fā)一個(gè)功能,用于判斷某年是否為閏年。創(chuàng)建 leap_year.py 文件。
# _*_ coding:utf-8 _*_
"""
name:zhangxingzai
date:2023/2/23
"""
class LeapYear:
"""計(jì)算某年是否為閏年"""
def __init__(self, year):
self.year = int(year)
def answer(self):
year = self.year
if year % 100 == 0:
if year % 400 == 0: # 整百年能被 400 整除的是閏年
return "{0}是閏年".format(year)
else:
return "{0}不是閏年".format(year)
else:
if year % 4 == 0: # 非整百年能被 4 整除的是閏年
return "{0}是閏年".format(year)
else:
return "{0}不是閏年".format(year)
創(chuàng)建對(duì)應(yīng)的測(cè)試文件 test_leap_year.py
# _*_ coding:utf-8 _*_
"""
name:zhangxingzai
date:2023/2/23
"""
import unittest
from leap_year import LeapYear
class TestLeapYear(unittest.TestCase):
def test_2000(self):
ly = LeapYear(2000)
self.assertEqual(ly.answer(), "2000是閏年")
def test_2004(self):
ly = LeapYear(2004)
self.assertEqual(ly.answer(), "2004是閏年")
def test_2017(self):
ly = LeapYear(2017)
self.assertEqual(ly.answer(), "2017不是閏年")
def test_2100(self):
ly = LeapYear(2100)
self.assertEqual(ly.answer(), "2100不是閏年")
if __name__ == '__main__':
unittest.main()
顯然,這里的判斷閏年功能(leap_year.py)和計(jì)算器功能(calculator.py)并不相關(guān),
它們的代碼分別寫在兩個(gè)文件當(dāng)中,所以對(duì)應(yīng)的測(cè)試用例最好分開,分別為test_calculator.py
和 test_leap_year.py。
當(dāng)前目錄結(jié)構(gòu)如下:

如何執(zhí)行多個(gè)測(cè)試文件呢?unittest中的TestLoader類提供的discover()方法可以從多個(gè)
文件中查找測(cè)試用例。
該類根據(jù)各種標(biāo)準(zhǔn)加載測(cè)試用例,并將它們返回給測(cè)試套件。正常情況下,不需要?jiǎng)?chuàng)
建這個(gè)類的實(shí)例。unittest 提供了可以共享的 defaultTestLoader 類,可以使用其子類或方法
創(chuàng)建實(shí)例,discover()方法就是其中之一。
discover(start_dir,pattern='test*.py',top_level_dir=None)
找到指定目錄及其子目錄下的所有測(cè)試模塊,只有匹配的文件名才能被加載。如果啟
動(dòng)的不是頂層目錄,那么頂層目錄必須單獨(dú)指定。
start_dir :待測(cè)試的模塊名或測(cè)試用例目錄。
pattern='test*.py' :測(cè)試用例文件名的匹配原則。此處匹配文件名以“test”開頭
的“.py”類型的文件,星號(hào)“*”表示任意多個(gè)字符。
top_level_dir=None:測(cè)試模塊的頂層目錄,如果沒有頂層目錄,則默認(rèn)為 None。
現(xiàn)在通過 discover()方法重新實(shí)現(xiàn) run_tests.py 文件的功能。
import unittest
# 定義測(cè)試用例的目錄為當(dāng)前目錄中的AutoFunction/目錄
test_dir = './AutoFunction'
suits = unittest.defaultTestLoader.discover(test_dir, pattern='test*.py')
if __name__ == '__main__':
runner = unittest.TextTestRunner()
runner.run(suits)
discover()方法會(huì)自動(dòng)根據(jù)測(cè)試用例目錄(test_dir)查找測(cè)試用例文件(test*.py),并
將找到的測(cè)試用例添加到測(cè)試套件中,因此,可以直接通過 run()方法執(zhí)行測(cè)試套件 suits。
這種方式極大地簡(jiǎn)化了測(cè)試用例的查找,我們需要做的就是按照文件的匹配規(guī)則創(chuàng)建測(cè)試
文件即可。
6.測(cè)試用例的執(zhí)行順序
測(cè)試用例的執(zhí)行順序涉及多個(gè)層級(jí):多個(gè)測(cè)試目錄 > 多個(gè)測(cè)試文件 > 多個(gè)測(cè)試類 >
多個(gè)測(cè)試方法(測(cè)試用例)。unittest 提供的 main()方法和 discover()方法是按照什么順序查
找測(cè)試用例的呢?
我們先運(yùn)行一個(gè)例子,再解釋 unittest 的執(zhí)行策略。
# _*_ coding:utf-8 _*_
"""
name:zhangxingzai
date:date:2023/2/23
"""
import unittest
class TestBdd(unittest.TestCase):
def setUp(self):
print('test TestBdd:')
def test_ccc(self):
print('test ccc')
def test_aaa(self):
print('test aaa')
class TestAdd(unittest.TestCase):
def setUp(self):
print("test TestAdd:")
def test_bbb(self):
print("test bbb")
if __name__ == '__main__':
unittest.main()
執(zhí)行結(jié)果如下:

無論執(zhí)行多少次,結(jié)果都是一樣的。通過上面的結(jié)果,相信你已經(jīng)找到 main()方法執(zhí)
行測(cè)試用例的規(guī)律了。
因?yàn)閡nittest默認(rèn)根據(jù)ASCII碼的順序加載測(cè)試用例的(數(shù)字與字母的順序?yàn)?~9,A~Z,
a~z),所以 TestAdd 類會(huì)優(yōu)先于 TestBdd 類被執(zhí)行,test_aaa()方法會(huì)優(yōu)先于 test_ccc()方法
被執(zhí)行,也就是說,它并不是按照測(cè)試用例的創(chuàng)建順序從上到下執(zhí)行的。
discover()方法和 main()方法的執(zhí)行順序是一樣的。對(duì)于測(cè)試目錄與測(cè)試文件來說,上
面的規(guī)律同樣適用。test_aaa.py 文件會(huì)優(yōu)先于 test_bbb.py 文件被執(zhí)行。所以,如果想讓某
個(gè)測(cè)試文件先執(zhí)行,可以在命名上加以控制。
除命名外,有沒有其他辦法控制測(cè)試用例的執(zhí)行順序呢?答案是肯定的,前面也有介
紹,我們可以聲明測(cè)試套件 TestSuite 類,通過 addTest()方法按照一定的順序來加載測(cè)試用
例。
修改上面的例子如下:
# _*_ coding:utf-8 _*_
"""
name:zhangxingzai
date:date:2023/2/23
"""
import unittest
class TestBdd(unittest.TestCase):
def setUp(self):
print('test TestBdd:')
def test_ccc(self):
print('test ccc')
def test_aaa(self):
print('test aaa')
class TestAdd(unittest.TestCase):
def setUp(self):
print("test TestAdd:")
def test_bbb(self):
print("test bbb")
if __name__ == '__main__':
# 構(gòu)造測(cè)試集
suite = unittest.TestSuite()
suite.addTest(TestBdd("test_aaa"))
suite.addTest(TestAdd("test_bbb"))
suite.addTest(TestBdd("test_ccc"))
# 執(zhí)行測(cè)試
runner = unittest.TextTestRunner()
runner.run(suite)
執(zhí)行結(jié)果如下:

現(xiàn)在的執(zhí)行順序與 addTest()方法加載測(cè)試用例的順序相同。不過,當(dāng)測(cè)試用例非常多
時(shí),不推薦用這種方法創(chuàng)建測(cè)試套件,原因前面也有說明,最好的方法是通過命名控制執(zhí)
行順序。如果測(cè)試用例在設(shè)計(jì)時(shí)不產(chǎn)生相互依賴,那么測(cè)試用例的執(zhí)行順序就沒那么重要
了。
7.跳過測(cè)試和預(yù)期失敗
在運(yùn)行測(cè)試時(shí),有時(shí)需要直接跳過某些測(cè)試用例,或者當(dāng)測(cè)試用例符合某個(gè)條件時(shí)跳
過測(cè)試,又或者直接將測(cè)試用例設(shè)置為失敗。unittest 提供了實(shí)現(xiàn)這些需求的裝飾器。
無條件地跳過裝飾的測(cè)試,需要說明跳過測(cè)試的原因:
unittest.skip(reason)
如果條件為真,則跳過裝飾的測(cè)試:
unittest.skipIf(condition, reason)
當(dāng)條件為真時(shí),執(zhí)行裝飾的測(cè)試:
unittest.skipUnless(condition, reason)
不管執(zhí)行結(jié)果是否失敗,都將測(cè)試標(biāo)記為失?。?/p>
unittest.expectedFailure()
舉例如下:
import unittest
class MyTest(unittest.TestCase):
@unittest.skip("直接跳過測(cè)試")
def test_skip(self):
print("test aaa")
@unittest.skipIf(3 > 2, "當(dāng)條件為真時(shí)跳過測(cè)試")
def test_skip_if(self):
print('test bbb')
@unittest.skipUnless(3 > 2, "當(dāng)條件為真時(shí)執(zhí)行測(cè)試")
def test_skip_unless(self):
print('test ccc')
@unittest.expectedFailure
def test_expected_failure(self):
self.assertEqual(2, 3)
if __name__ == '__main__':
unittest.main()
執(zhí)行結(jié)果如下:
Skipped: 直接跳過測(cè)試
Skipped: 當(dāng)條件為真時(shí)跳過測(cè)試
test ccc
Ran 4 tests in 0.010s
OK (skipped=2, expected failures=1)
上面的例子創(chuàng)建了四條測(cè)試用例。
第一條測(cè)試用例通過@unittest.skip()裝飾,直接跳過測(cè)試。
第二條測(cè)試用例通過@unittest.skipIf()裝飾,當(dāng)條件為真時(shí)跳過測(cè)試;3>2 條件為真
(True),所以跳過測(cè)試。
第三條測(cè)試用例通過@unittest.skipUnless()裝飾,當(dāng)條件為真時(shí)執(zhí)行測(cè)試;3>2 條件為
真(True),執(zhí)行測(cè)試。
第四條測(cè)試用例通過@unittest.expectedFailure 裝飾,不管執(zhí)行結(jié)果是否失敗,都將測(cè)
試標(biāo)記為失敗,但不會(huì)拋出失敗信息。
8.Fixtuer(測(cè)試夾具)
我們可以把 Fixture 看作夾心餅干外層的兩片餅干,這兩片餅干就是 setUp/tearDown,
中間的奶油就是測(cè)試用例。除此之外,unittest 還提供了更大范圍的 Fixture,如測(cè)試類和模
塊的 Fixture。
# _*_ coding:utf-8 _*_
"""
name:zhangxingzai
"""
import unittest
def setUpModule():
print("test module start >>>>>>>>>>>>>>")
def tearDownModule():
print("test module end >>>>>>>>>>>>>>")
class MyTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
print("test class start =======>")
@classmethod
def tearDownClass(cls):
print("test class end =======>")
def setUp(self):
print("test case start -->")
def tearDown(self):
print("test case end -->")
def test_case1(self):
print("test case1")
def test_case2(self):
print("test case2")
if __name__ == '__main__':
unittest.main()
執(zhí)行結(jié)果如下:
test module start >>>>>>>>>>>>>>
test class start =======>
test case start -->
test case1
test case end -->
test case start -->
test case2
test case end -->
test class end =======>
test module end >>>>>>>>>>>>>>
Ran 2 tests in 0.005s
OK
setUpModule/tearDownModule:在整個(gè)模塊的開始與結(jié)束時(shí)被執(zhí)行。
setUpClass/tearDownClass:在測(cè)試類的開始與結(jié)束時(shí)被執(zhí)行。
setUp/tearDown:在測(cè)試用例的開始與結(jié)束時(shí)被執(zhí)行。
需要注意的是,setUpClass/tearDownClass 為類方法,需要通過@classmethod 進(jìn)行裝飾。
另外,方法的參數(shù)為 cls。其實(shí),cls 與 self 并沒有什么本質(zhì)區(qū)別,都只表示方法的第一個(gè)文章來源:http://www.zghlxwxcb.cn/news/detail-404577.html
參數(shù)。文章來源地址http://www.zghlxwxcb.cn/news/detail-404577.html
到了這里,關(guān)于【unittest學(xué)習(xí)】unittest框架主要功能的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!