Python測試-unittest椎工,2022-11-27

(2022.11.27 Sun)
unittest是Python自帶的單元測試框架。unittest+html和pytest+allure(測試報(bào)告)成為常用的自動(dòng)測試和報(bào)告的框架組合送浊。


unittest architecture

概念

  • test case測試用例:測試用例是測試的基本單元博秫,用于測試一組特定輸入的特定響應(yīng),unittest提供了基類unittest.TestCase用于創(chuàng)建測試案例异希。案例包括“輸入用戶名不輸入密碼健盒,則提示密碼為空”等。
  • test fixture測試腳手架:為開展測試需要進(jìn)行的準(zhǔn)備工作称簿,以及所有相關(guān)的清理操作(cleanup actions)扣癣,比如創(chuàng)建臨時(shí)或代理數(shù)據(jù)庫、目錄憨降,啟動(dòng)一個(gè)服務(wù)器進(jìn)程等父虑。
  • test suite測試套件:一系列的測試用例或測試套件,用于整合一些一起執(zhí)行的測試授药。
  • test runner測試運(yùn)行器:用于執(zhí)行和輸出測試結(jié)果的組件士嚎,可使用圖形接口呜魄、文本接口,或返回運(yùn)行測試結(jié)果的特定值莱衩。

案例

# unittest_basic_example01.py
import logging
import unittest

class Login:
    pass

class TestStringMethods(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        # 必須使用@classmethod 裝飾器,所有test運(yùn)行前運(yùn)行一次
        super().setUpClass()
        logging.info("setUpClass")

    @classmethod
    def tearDownClass(cls):
        # 必須使用@classmethod, 所有test運(yùn)行完后運(yùn)行一次
        super().tearDownClass()
        logging.info("tearDownClass")

    def setUp(self):
        # 每個(gè)測試用例執(zhí)行之后做操作
        # do preparation
        super().setUp()
        logging.info("setUp")

    def tearDown(self):
        # 每個(gè)測試用例執(zhí)行之前做操作
        super().tearDown()
        logging.info("tearDown")

    def test_upper(self):
        logging.info('method test_upper is in progress')
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        logging.info('method test_isupper is in progress')
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        logging.info('method test_split is in progress')
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO) #, datefmt="%H:%M:%S.%f"    
    unittest.main()

運(yùn)行結(jié)果

$ python unittest_basic_example01.py 
2022-11-27 14:48:37,872: setUpClass
2022-11-27 14:48:37,873: setUp
2022-11-27 14:48:37,873: method test_isupper is in progress
2022-11-27 14:48:37,873: tearDown
.2022-11-27 14:48:37,873: setUp
2022-11-27 14:48:37,873: method test_split is in progress
2022-11-27 14:48:37,873: tearDown
.2022-11-27 14:48:37,873: setUp
2022-11-27 14:48:37,873: method test_upper is in progress
2022-11-27 14:48:37,873: tearDown
.2022-11-27 14:48:37,873: tearDownClass

----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

該案例給出若干測試用例的結(jié)果和方法的執(zhí)行順序爵嗅。

案例中定義了單元測試類TestStringMethods,在import unittest之后笨蚁,定義該單元測試類之時(shí)睹晒,繼承unittest.TestCase,使得該類成為一個(gè)unittest類括细。

unittest.TestCase有四個(gè)基本方法

  • setUpClass
  • setUp
  • tearDownClass
  • tearDown

注意這四個(gè)方法針對不同的測試用例和測試用例類册招,兩兩成一對,即setUptearDown勒极,setUpClasstearDownClass是掰。

setUpClasstearDownClass都用@classmethod裝飾器裝飾為類方法,這兩個(gè)方法分別在TestStringMethods的所有test case之前和之后運(yùn)行一次辱匿。

setUptearDown則針對test case而言键痛,每個(gè)test case執(zhí)行前、后分別執(zhí)行這兩個(gè)方法匾七。用于做測試的準(zhǔn)備和收尾工作絮短。

(2022.12.17 Sat)這四個(gè)方法都用于對測試的準(zhǔn)備工作,如setUpClasstearDownClass用于在測試類對象開始執(zhí)行之前對類做初始化準(zhǔn)備工作和收尾工作(如關(guān)閉什么)昨忆。setUptearDown針對test case做初始化和收尾工作丁频。

test case都以test作為方法的開始,這個(gè)命名傳統(tǒng)用于向test runner通知哪些方法代表著test case邑贴。

觀察測試結(jié)果席里,兩個(gè)test case中, test_isupper先于test_split執(zhí)行拢驾。默認(rèn)情況下奖磁,test case的執(zhí)行順序?yàn)榉椒淖值湫?alphabetical)。同時(shí)還有其他若干方法可以調(diào)整test case的執(zhí)行順序繁疤。

斷言assert

test case中最常用的斷言方法

method checks that new in
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 3.1
assertIsNot(a, b) a is not b 3.1
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b 3.1
assertNotIn(a, b) a not in b 3.1
assertIsInstance(a, b) isinstance(a, b) 3.2
assertNotIsInstance(a, b) not isinstance(a, b) 3.2
assertRaises(xxxError)

如何測試拋出異常 How to Raise an exception in unit test

(2023.02.11 Sat)
使用unittest中的assertRaises方法咖为。考慮下面案例test_add_fish_to_aquarium.py

import unittest

def add_fish_to_aquarium(fish_list):
    if len(fish_list) > 10:
        raise ValueError("A maximum of 10 fish can be added to the aquarium")
    return {"tank_a": fish_list}

class TestAddFishToAquarium(unittest.TestCase):
    def test_add_fish_to_aquarium_success(self):
        actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
        expected = {"tank_a": ["shark", "tuna"]}
        self.assertEqual(actual, expected)

    def test_add_fish_to_aquarium_exception(self):
        too_many_fish = ["shark"] * 25
        with self.assertRaises(ValueError) as exception_context:
            add_fish_to_aquarium(fish_list=too_many_fish)
        self.assertEqual(
            str(exception_context.exception),
            "A maximum of 10 fish can be added to the aquarium"
        )

在該案例中被測試函數(shù)add_fish_to_aquarium檢測輸入變量長度稠腊,如果超過10則返回ValueError躁染,和提示信息。在測試部分架忌,使用context manager吞彤,執(zhí)行被測試函數(shù),即

with self.assertRaises(ValueError) as exception_context:
    add_fish_to_aquarium(fish_list=too_many_fish)

此時(shí)會(huì)拋出異常并保存在異常對象exception_context中鳖昌。接下來判斷異常對象中內(nèi)容和函數(shù)內(nèi)的異常信息是否一致

self.assertEqual(str(exception_context.exception), 'A maximum of 10 fish can be added to the aquarium')

至此可以實(shí)現(xiàn)測試代碼中對異常的測試备畦。運(yùn)行在終端該文本

>> python -m unittest test_add_fish_to_aquarium.py
Output
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

TestCase的執(zhí)行順序

(2022.12.17 Sat)
unittest中各個(gè)test case的執(zhí)行順序如前面所述,按照以test_開始的test case名字的字典順序執(zhí)行许昨。比如上一部分的案例中懂盐,test case共三個(gè)test_uppertest_isupper糕档、test_split莉恼。按test_之后名稱的字典序,則其排序?yàn)?code>test_isupper速那、test_splittest_upper俐银,而這也是運(yùn)行結(jié)果中test case的排序。

除此之外端仰,還有其他方法可以設(shè)定test case的排序

加序號

在test case的名字中加入預(yù)先設(shè)定的序號捶惜,執(zhí)行時(shí)按照序號的順序執(zhí)行。

import logging
import unittest

class SeqOrder(unittest.TestCase):

  def test_3(self):
      logging.info('method step3')
      
  def test_1(self):
      logging.info('method step1')

  def test_2(self):
      logging.info('method step2')

if __name__ == '__main__':
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO) #, datefmt="%H:%M:%S.%f"    
    unittest.main()

運(yùn)行結(jié)果為

 % python unittest_basic_example03.py
2022-12-17 12:07:58,319: method step1
.2022-12-17 12:07:58,319: method step2
.2022-12-17 12:07:58,319: method step3
.
----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

Monolithic test

將test case結(jié)合為一個(gè)整體荔烧,運(yùn)行時(shí)按整體內(nèi)部的test case排序運(yùn)行吱七。在下面案例中,test case名字不以test_作為開頭鹤竭,但經(jīng)過self._steps方法排序(dir(self))踊餐,在執(zhí)行時(shí)(test_steps),調(diào)用了self._steps生成器臀稚,依次執(zhí)行test case吝岭。這種方法類似于在test case的名字中按開發(fā)者意圖加入序號并按序號執(zhí)行。

import logging
import unittest

class Monolithic(unittest.TestCase):

  def step3(self):
      logging.info('method step3')
      
  def step1(self):
      logging.info('method step1')

  def step2(self):
      logging.info('method step2')

  def _steps(self):
    for name in dir(self): # dir() result is implicitly sorted
      if name.startswith("step"):
        yield name, getattr(self, name) 

  def test_steps(self):
    for name, step in self._steps():
      try:
        step()
      except Exception as e:
        self.fail("{} failed ({}: {})".format(step, type(e), e))

if __name__ == '__main__':
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO) #, datefmt="%H:%M:%S.%f"    
    unittest.main()

運(yùn)行結(jié)果

 % python unittest_basic_example02.py 
2022-12-17 11:34:38,555: method step1
2022-12-17 11:34:38,556: method step2
2022-12-17 11:34:38,556: method step3
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

上面代碼中dir(self)返回該類的內(nèi)部對象吧寺,并按序輸出窜管。

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', 
'__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', 
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 
'__weakref__', '_steps', 'step1', 'step2', 'step3', 'test_steps']

TestSuite

在test suite中加入test case,加入順序即執(zhí)行順序稚机。

import logging
import unittest

class TestOrder(unittest.TestCase):

  def test_1(self):
      logging.info('method step1')

  def test_2(self):
      logging.info('method step2')


class OtherOrder(unittest.TestCase):

  def test_4(self):
      logging.info('method step4')
      
  def test_3(self):
      logging.info('method step3')


def suite():
    suite = unittest.TestSuite()
    suite.addTest(OtherOrder('test_4'))
    suite.addTest(TestOrder('test_2'))
    suite.addTest(OtherOrder('test_3'))
    suite.addTest(TestOrder('test_1'))
    return suite

if __name__ == '__main__':
    format = "%(asctime)s: %(message)s"
    logging.basicConfig(format=format, level=logging.INFO) #, datefmt="%H:%M:%S.%f"    
    runner = unittest.TextTestRunner(failfast=True)
    runner.run(suite())

運(yùn)行結(jié)果如下

% python unittest_basic_example04.py
2022-12-17 12:24:12,820: method step4
.2022-12-17 12:24:12,820: method step2
.2022-12-17 12:24:12,820: method step3
.2022-12-17 12:24:12,820: method step1
.
----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

Reference

1 經(jīng)驗(yàn)分享:自動(dòng)化測試框架之unittest微峰,測試小濤
2 unittest教程(2w字實(shí)例合集)——Python自動(dòng)化測試一文入門,是羽十八ya
3 python unittest official doc
4 Python unittest.TestCase execution order, stackoverflow

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抒钱,一起剝皮案震驚了整個(gè)濱河市蜓肆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌谋币,老刑警劉巖仗扬,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蕾额,居然都是意外死亡早芭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門诅蝶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來退个,“玉大人募壕,你說我怎么就攤上這事∮镉” “怎么了舱馅?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長刀荒。 經(jīng)常有香客問我代嗤,道長,這世上最難降的妖魔是什么缠借? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任干毅,我火速辦了婚禮,結(jié)果婚禮上泼返,老公的妹妹穿的比我還像新娘硝逢。我一直安慰自己,他們只是感情好绅喉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布趴捅。 她就那樣靜靜地躺著,像睡著了一般霹疫。 火紅的嫁衣襯著肌膚如雪拱绑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天丽蝎,我揣著相機(jī)與錄音猎拨,去河邊找鬼。 笑死屠阻,一個(gè)胖子當(dāng)著我的面吹牛红省,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播国觉,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼吧恃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了麻诀?” 一聲冷哼從身側(cè)響起痕寓,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蝇闭,沒想到半個(gè)月后呻率,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡呻引,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年礼仗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡元践,死狀恐怖韭脊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情单旁,我是刑警寧澤沪羔,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站慎恒,受9級特大地震影響任内,放射性物質(zhì)發(fā)生泄漏撵渡。R本人自食惡果不足惜融柬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望趋距。 院中可真熱鬧粒氧,春花似錦、人聲如沸节腐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽翼雀。三九已至饱苟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間狼渊,已是汗流浹背箱熬。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狈邑,地道東北人城须。 一個(gè)月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像米苹,于是被迫代替她去往敵國和親糕伐。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容