什么是單元測試
單元測試是用來對最小可測試單元(如一個函數(shù)、一個類或者一個模塊)進行檢查和驗證。尤其是在對代碼進行修改或重構后,可以快速準確的定位出現(xiàn)問題的問題。
為什么要做單元測試
寫代碼的首要目標就是實現(xiàn)需求寄锐。在此基礎上,就要盡可能提高代碼的質量和可維護性尖啡。而單元測試橄仆,就是提高代碼質量和可維護性的一種方式,也是成本最低的一種方式衅斩。因為盡早排除盡可能過的bug盆顾,可以減少在后續(xù)階段解決bug的成本(包括時間成本、人力成本等)畏梆。
單元測試的基本原則
- 單元測試代碼要非常簡單您宪,如果測試代碼過于復雜奈懒,則測試代碼本身也可能存在bug
- 通過單元測試,并不意味著所測代碼沒有bug
Python單元測試框架
- Unittest:Python標準庫自帶的單元測試工具宪巨,也稱為PyUnit磷杏,是JUnit(Java語言的單元測試)的Python版本。
- Doctest:Python標準庫自帶的單元測試工具捏卓,適合簡單的單元測試极祸。其工作原理是在docstring中尋找測試用例并執(zhí)行,比較輸出結果與期望值是否符合怠晴。
- Pytest:Python的一種單元測試框架遥金,與Unittest測試框架類似。其特點是擁有眾多高級功能和插件蒜田。
- Nose:繼承自Unittest稿械,但比Unittest易用,而且能夠自動識別測試用例物邑。任何符合正則表達式“(?:^|[b_.-])[Tt]est”的類溜哮、函數(shù)、文件或目錄色解,以及TestCase的子類都會被識別并執(zhí)行。
- Unittest2:Unittest的升級版餐茵。
Unittest
Unittest是Python的標準測試庫科阎,無需額外安裝,是目前Python使用較為官方的測試框架忿族。下面通過實例锣笨,一步一步了解如何使用Unittest。
待測試代碼如下道批,用于驗證email和密碼错英。
# -*- coding: utf-8 -*-
import re
class CheckUserInfo:
def check_mail(self, mail):
flag = False
str = '^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+){0,4}@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+){0,4}$'
if re.match(str, mail):
flag = True
return flag
def check_pwd_len(self, pwd):
'''密碼長度不超過8位'''
return len(pwd)>=8
def check_pwd_contain_leter(self, pwd):
'''密碼包含大小寫英文字母'''
flag = False
pattern = re.compile('[A-Z][a-z]+')
match = pattern.findall(pwd)
if match:
flag = True
return flag
def check_pwd_contain_num(self, pwd):
'''密碼包含數(shù)字'''
flag = False
pattern = re.compile('[0-9]+')
match = pattern.findall(pwd)
if match:
flag = True
return flag
TestCase — 單元測試用例
對上面代碼中的每個方法進行測試,就需要實現(xiàn)一個繼承unittest.TestCase 測試用例類隆豹。
# -*- coding: utf-8 -*-
import unittest
from com.unit_test.check_user_info import CheckUserInfo
class CheckUserInfoTestCase(unittest.TestCase):
def __init__(self, *args, **kwargs):
super(CheckUserInfoTestCase, self).__init__(*args, **kwargs)
self.check_user_info = CheckUserInfo()
@classmethod
def setUpClass(cls):
print('setUpClass\n\n')
@classmethod
def tearDownClass(cls):
print('tearDownClass')
def setUp(self):
print('setUp')
def tearDown(self):
print('tearDown\n')
def test_check_mail(self):
print('test_check_mail')
self.assertEqual(True, self.check_user_info.check_mail('aa@bb'))
self.assertEqual(False, self.check_user_info.check_mail('aa.bb'))
def test_check_pwd_len(self):
print('test_check_pwd_len')
self.assertEqual(True, self.check_user_info.check_pwd_len('12345678'))
self.assertEqual(False, self.check_user_info.check_pwd_len(''))
self.assertEqual(False, self.check_user_info.check_pwd_len('1'))
self.assertEqual(True, self.check_user_info.check_pwd_len('123456789'))
def test_check_pwd_contain_letter(self):
print('test_check_pwd_contain_leter')
self.assertEqual(True, self.check_user_info.check_pwd_contain_leter('1qazXSW@'))
self.assertEqual(False, self.check_user_info.check_pwd_contain_leter('12345678'))
self.assertEqual(False, self.check_user_info.check_pwd_contain_leter(''))
def test_check_pwd_contain_num(self):
print('test_check_pwd_contain_num')
self.assertEqual(True, self.check_user_info.check_pwd_contain_num('1qazXSW@'))
self.assertEqual(False, self.check_user_info.check_pwd_contain_num('qwertasdfg'))
self.assertEqual(False, self.check_user_info.check_pwd_contain_num(''))
def aaa(self):
print('test_check_pwd_contain_num')
self.assertEqual(True, self.check_user_info.check_pwd_contain_num('1qazXSW@'))
if __name__ == '__main__':
unittest.main()
執(zhí)行結果如下:
...
setUpClass
setUp
test_check_mail
tearDown
setUp
test_check_pwd_contain_leter
tearDown
setUp
test_check_pwd_contain_num
tearDown
setUp
test_check_pwd_len
tearDown
tearDownClass
Ran 4 tests in 0.013s
OK
Process finished with exit code 0
使用“unittest.main()”椭岩,通過觀察上面的測試代碼以及執(zhí)行結果,可以發(fā)現(xiàn):
testXXX方法
使用“unittest.main()”執(zhí)行測試用例時璃赡,每個測試方法必須以“test”開頭判哥,否則Unittes無法識別。例如碉考,上面代碼中的“aaa”方法未執(zhí)行塌计。
測試方法執(zhí)行順序
每個測試方法執(zhí)行順序并不是按照代碼的書寫順序執(zhí)行,而是按照前綴“test”后的字母順序執(zhí)行的侯谁。
setUp()方法
setUp()方法一般用來準備測試環(huán)境锌仅,在每個測試方法執(zhí)行之前執(zhí)行章钾。
tearDown()方法
tearDown()方法一般用來清理測試環(huán)境,在每個測試方法執(zhí)行之后執(zhí)行热芹,且不論測試方法執(zhí)行是否成功贱傀。
setUpClass()方法
setUpClass()方法一般用來在所有測試方法執(zhí)行之前準備一次測試環(huán)境,必須使用“@classmethod”裝飾符剿吻,在所有測試方法執(zhí)行前執(zhí)行窍箍。
tearDownClass()
tearDownClass()方法一般用來在所有測試方法結束之后清理一次測試環(huán)境,必須使用“@classmethod”裝飾符丽旅,在所有測試方法都執(zhí)行完成否執(zhí)行椰棘。
skip裝飾器
不希望執(zhí)行某個測試方法時,可以使用skip裝飾器榄笙。skip裝飾器共有三種:unittest.skip(reason)邪狞、unittest.skipIf(condition, reason)、unittest.skipUnless(condition, reason)茅撞。
- unittest.skip(reason):無條件跳過
# -*- coding: utf-8 -*-
import unittest
from com.unit_test.check_user_info import CheckUserInfo
class CheckUserInfoTestCase(unittest.TestCase):
......
@unittest.skip('skip test_bbb method')
def test_bbb(self):
print('test_bbb')
......
部分執(zhí)行結果如下:
...
Skipped: skip test_bbb method
...
- unittest.skipIf(condition, reason):當condition為True時跳過
# -*- coding: utf-8 -*-
import unittest
from com.unit_test.check_user_info import CheckUserInfo
class CheckUserInfoTestCase(unittest.TestCase):
......
@unittest.skipIf(1<2, 'skip test_ccc method')
def test_ccc(self):
print('test_ccc')
......
部分執(zhí)行結果如下:
...
Skipped: skip test_ccc method
...
- unittest.skipUnless(condition, reason):當condition為False時跳過
# -*- coding: utf-8 -*-
import unittest
from com.unit_test.check_user_info import CheckUserInfo
class CheckUserInfoTestCase(unittest.TestCase):
......
@unittest.skipUnless(1>2, 'skip test_ddd method')
def test_ddd(self):
print('test_ddd')
......
部分執(zhí)行結果如下:
...
Skipped: skip test_ddd method
...
那么帆卓,如何控制測試方法的執(zhí)行順序呢?如何執(zhí)行非“test”開頭的測試方法米丘?接下來了解一下TestSuite剑令。
TestSuite — 單元測試用例合集
通過上面的測試用例代碼,可以發(fā)現(xiàn)無法指定測試方法的順序拄查。那么通過unittest.TestSuite吁津,就可以按照業(yè)務邏輯順序執(zhí)行測試方法。
# -*- coding: utf-8 -*-
import unittest
from com.unit_test.check_user_info_test_case import CheckUserInfoTestCase
if __name__ == '__main__':
suite = unittest.TestSuite()
tests = [
CheckUserInfoTestCase('test_check_mail'),
CheckUserInfoTestCase('test_check_pwd_len'),
CheckUserInfoTestCase('test_check_pwd_contain_letter'),
CheckUserInfoTestCase('test_check_pwd_contain_num')
]
suite.addTests(tests)
runner = unittest.TextTestRunner()
runner.run(suite)
執(zhí)行結果如下:
...
Ran 4 tests in 0.002s
OK
setUpClass
setUp
test_check_mail
tearDown
setUp
test_check_pwd_len
tearDown
setUp
test_check_pwd_contain_leter
tearDown
setUp
test_check_pwd_contain_num
tearDown
tearDownClass
Process finished with exit code 0
通過數(shù)組指定測試方法的執(zhí)行順序
tests = [
CheckUserInfoTestCase('test_check_mail'),
CheckUserInfoTestCase('test_check_pwd_len'),
CheckUserInfoTestCase('test_check_pwd_contain_letter'),
CheckUserInfoTestCase('test_check_pwd_contain_num')
]
suite.addTests(tests)
通過addTests()方法指定測試方法的執(zhí)行順序
suite.addTest(CheckUserInfoTestCase('test_check_mail'))
suite.addTest(CheckUserInfoTestCase('test_check_pwd_len'))
suite.addTest(CheckUserInfoTestCase('test_check_pwd_contain_letter'))
suite.addTest(CheckUserInfoTestCase('test_check_pwd_contain_num'))
無法指定測試方法執(zhí)行順序的unittest.TestLoader()
通過unittest.TestLoader()加載TestCase堕扶,測試方法是按照前綴“test”后的字母順序執(zhí)行的碍脏。
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(CheckUserInfoTestCase))
執(zhí)行非“test”開頭的測試方法
在TestCase中的示例代碼中,可以看到稍算,測試方法名不是以“test”開頭時是不會被執(zhí)行的典尾。那么,通過TestSuite就可以完美解決這個問題糊探。
suite.addTest(CheckUserInfoTestCase('aaa'))