Python單元測試(一)--Unittest

什么是單元測試

單元測試是用來對最小可測試單元(如一個函數(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'))
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末钾埂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子侧到,更是在濱河造成了極大的恐慌勃教,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匠抗,死亡現(xiàn)場離奇詭異故源,居然都是意外死亡,警方通過查閱死者的電腦和手機汞贸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門绳军,熙熙樓的掌柜王于貴愁眉苦臉地迎上來印机,“玉大人,你說我怎么就攤上這事门驾∩淙” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵奶是,是天一觀的道長楣责。 經常有香客問我,道長聂沙,這世上最難降的妖魔是什么秆麸? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮及汉,結果婚禮上沮趣,老公的妹妹穿的比我還像新娘。我一直安慰自己坷随,他們只是感情好房铭,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著温眉,像睡著了一般缸匪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上类溢,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天豪嗽,我揣著相機與錄音,去河邊找鬼豌骏。 笑死,一個胖子當著我的面吹牛隐锭,可吹牛的內容都是我干的窃躲。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼钦睡,長吁一口氣:“原來是場噩夢啊……” “哼蒂窒!你這毒婦竟也來了?” 一聲冷哼從身側響起荞怒,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤洒琢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后褐桌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體衰抑,經...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年荧嵌,在試婚紗的時候發(fā)現(xiàn)自己被綠了呛踊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片砾淌。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖谭网,靈堂內的尸體忽然破棺而出汪厨,到底是詐尸還是另有隱情,我是刑警寧澤愉择,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布劫乱,位于F島的核電站,受9級特大地震影響锥涕,放射性物質發(fā)生泄漏衷戈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一站楚、第九天 我趴在偏房一處隱蔽的房頂上張望脱惰。 院中可真熱鬧,春花似錦窿春、人聲如沸拉一。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔚润。三九已至,卻和暖如春尺栖,著一層夾襖步出監(jiān)牢的瞬間嫡纠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工延赌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留除盏,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓挫以,卻偏偏與公主長得像者蠕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子掐松,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容