Python必會的單元測試框架 —— unittest

用Python搭建自動化測試框架,我們需要組織用例以及測試執(zhí)行,這里博主推薦Python的標(biāo)準(zhǔn)庫——unittest。

unittest是xUnit系列框架中的一員,如果你了解xUnit的其他成員,那你用unittest來應(yīng)該是很輕松的,它們的工作方式都差不多。

unittest核心工作原理

unittest中最核心的四個概念是:test case, test suite, test runner, test fixture。

下面我們分別來解釋這四個概念的意思,先來看一張unittest的靜態(tài)類圖(下面的類圖以及解釋均來源于網(wǎng)絡(luò),原文鏈接):

unittest類圖
  • 一個TestCase的實例就是一個測試用例。什么是測試用例呢?就是一個完整的測試流程,包括測試前準(zhǔn)備環(huán)境的搭建(setUp),執(zhí)行測試代碼(run),以及測試后環(huán)境的還原(tearDown)。元測試(unit test)的本質(zhì)也就在這里,一個測試用例是一個完整的測試單元,通過運行這個測試單元,可以對某一個問題進行驗證。

  • 而多個測試用例集合在一起,就是TestSuite,而且TestSuite也可以嵌套TestSuite。

  • TestLoader是用來加載TestCase到TestSuite中的,其中有幾個loadTestsFrom__()方法,就是從各個地方尋找TestCase,創(chuàng)建它們的實例,然后add到TestSuite中,再返回一個TestSuite實例。

  • TextTestRunner是來執(zhí)行測試用例的,其中的run(test)會執(zhí)行TestSuite/TestCase中的run(result)方法。
    測試的結(jié)果會保存到TextTestResult實例中,包括運行了多少測試用例,成功了多少,失敗了多少等信息。

  • 而對一個測試用例環(huán)境的搭建和銷毀,是一個fixture。

一個class繼承了unittest.TestCase,便是一個測試用例,但如果其中有多個以 test 開頭的方法,那么每有一個這樣的方法,在load的時候便會生成一個TestCase實例,如:一個class中有四個test_xxx方法,最后在load到suite中時也有四個測試用例。

到這里整個流程就清楚了:

寫好TestCase,然后由TestLoader加載TestCase到TestSuite,然后由TextTestRunner來運行TestSuite,運行的結(jié)果保存在TextTestResult中,我們通過命令行或者unittest.main()執(zhí)行時,main會調(diào)用TextTestRunner中的run來執(zhí)行,或者我們可以直接通過TextTestRunner來執(zhí)行用例。這里加個說明,在Runner執(zhí)行時,默認(rèn)將執(zhí)行結(jié)果輸出到控制臺,我們可以設(shè)置其輸出到文件,在文件中查看結(jié)果(你可能聽說過HTMLTestRunner,是的,通過它可以將結(jié)果輸出到HTML中,生成漂亮的報告,它跟TextTestRunner是一樣的,從名字就能看出來,這個我們后面再說)。

unittest實例

下面我們通過一些實例來更好地認(rèn)識一下unittest。

我們先來準(zhǔn)備一些待測方法:

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

簡單示例

接下來我們?yōu)檫@些方法寫一個測試:

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()

執(zhí)行結(jié)果:

.F..
======================================================================
FAIL: test_divide (__main__.TestMathFunc)
Test method divide(a, b)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:/py/test_mathfunc.py", line 26, in test_divide
    self.assertEqual(2.5, divide(5, 2))
AssertionError: 2.5 != 2

----------------------------------------------------------------------
Ran 4 tests in 0.000s

FAILED (failures=1)

能夠看到一共運行了4個測試,失敗了1個,并且給出了失敗原因,2.5 != 2 也就是說我們的divide方法是有問題的。

這就是一個簡單的測試,有幾點需要說明的:

  1. 在第一行給出了每一個用例執(zhí)行的結(jié)果的標(biāo)識,成功是 .,失敗是 F,出錯是 E,跳過是 S。從上面也可以看出,測試的執(zhí)行跟方法的順序沒有關(guān)系,test_divide寫在了第4個,但是卻是第2個執(zhí)行的。

  2. 每個測試方法均以 test 開頭,否則是不被unittest識別的。

  3. 在unittest.main()中加 verbosity 參數(shù)可以控制輸出的錯誤報告的詳細(xì)程度,默認(rèn)是 1,如果設(shè)為 0,則不輸出每一用例的執(zhí)行結(jié)果,即沒有上面的結(jié)果中的第1行;如果設(shè)為 2,則輸出詳細(xì)的執(zhí)行結(jié)果,如下:

test_add (__main__.TestMathFunc)
Test method add(a, b) ... ok
test_divide (__main__.TestMathFunc)
Test method divide(a, b) ... FAIL
test_minus (__main__.TestMathFunc)
Test method minus(a, b) ... ok
test_multi (__main__.TestMathFunc)
Test method multi(a, b) ... ok

======================================================================
FAIL: test_divide (__main__.TestMathFunc)
Test method divide(a, b)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:/py/test_mathfunc.py", line 26, in test_divide
    self.assertEqual(2.5, divide(5, 2))
AssertionError: 2.5 != 2

----------------------------------------------------------------------
Ran 4 tests in 0.002s

FAILED (failures=1)

可以看到,每一個用例的詳細(xì)執(zhí)行情況以及用例名,用例描述均被輸出了出來(在測試方法下加代碼示例中的"""Doc String""",在用例執(zhí)行時,會將該字符串作為此用例的描述,加合適的注釋能夠使輸出的測試報告更加便于閱讀

組織TestSuite

上面的代碼示例了如何編寫一個簡單的測試,但有兩個問題,我們怎么控制用例執(zhí)行的順序呢?(這里的示例中的幾個測試方法并沒有一定關(guān)系,但之后你寫的用例可能會有先后關(guān)系,需要先執(zhí)行方法A,再執(zhí)行方法B),我們就要用到TestSuite了。我們添加到TestSuite中的case是會按照添加的順序執(zhí)行的。

問題二是我們現(xiàn)在只有一個測試文件,我們直接執(zhí)行該文件即可,但如果有多個測試文件,怎么進行組織,總不能一個個文件執(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)

執(zhí)行結(jié)果:

test_add (test_mathfunc.TestMathFunc)
Test method add(a, b) ... ok
test_minus (test_mathfunc.TestMathFunc)
Test method minus(a, b) ... ok
test_divide (test_mathfunc.TestMathFunc)
Test method divide(a, b) ... FAIL

======================================================================
FAIL: test_divide (test_mathfunc.TestMathFunc)
Test method divide(a, b)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\py\test_mathfunc.py", line 26, in test_divide
    self.assertEqual(2.5, divide(5, 2))
AssertionError: 2.5 != 2

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

FAILED (failures=1)

可以看到,執(zhí)行情況跟我們預(yù)料的一樣:執(zhí)行了三個case,并且順序是按照我們添加進suite的順序執(zhí)行的。

上面用了TestSuite的 addTests() 方法,并直接傳入了TestCase列表,我們還可以:

# 直接用addTest方法添加單個TestCase
suite.addTest(TestMathFunc("test_multi"))

# 用addTests + TestLoader
# loadTestsFromName(),傳入'模塊名.TestCase名'
suite.addTests(unittest.TestLoader().loadTestsFromName('test_mathfunc.TestMathFunc'))
suite.addTests(unittest.TestLoader().loadTestsFromNames(['test_mathfunc.TestMathFunc']))  # loadTestsFromNames(),類似,傳入列表

# loadTestsFromTestCase(),傳入TestCase
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))

注意,用TestLoader的方法是無法對case進行排序的,同時,suite中也可以套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格式的測試報告了。

test fixture之setUp() tearDown()

上面整個測試基本跑了下來,但可能會遇到點特殊的情況:如果我的測試需要在每次執(zhí)行之前準(zhǔn)備環(huán)境,或者在每次執(zhí)行完之后需要進行一些清理怎么辦?比如執(zhí)行前需要連接數(shù)據(jù)庫,執(zhí)行完成之后需要還原數(shù)據(jù)、斷開連接??偛荒苊總€測試方法中都添加準(zhǔn)備環(huán)境、清理環(huán)境的代碼吧。

這就要涉及到我們之前說過的test fixture了,修改test_mathfunc.py

# -*- coding: utf-8 -*-

import unittest
from mathfunc import *


class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py"""

    def setUp(self):
        print "do something before test.Prepare environment."

    def tearDown(self):
        print "do something after test.Clean up."

    def test_add(self):
        """Test method add(a, b)"""
        print "add"
        self.assertEqual(3, add(1, 2))
        self.assertNotEqual(3, add(2, 2))

    def test_minus(self):
        """Test method minus(a, b)"""
        print "minus"
        self.assertEqual(1, minus(3, 2))

    def test_multi(self):
        """Test method multi(a, b)"""
        print "multi"
        self.assertEqual(6, multi(2, 3))

    def test_divide(self):
        """Test method divide(a, b)"""
        print "divide"
        self.assertEqual(2, divide(6, 3))
        self.assertEqual(2.5, divide(5, 2))

我們添加了 setUp()tearDown() 兩個方法(其實是重寫了TestCase的這兩個方法),這兩個方法在每個測試方法執(zhí)行前以及執(zhí)行后執(zhí)行一次,setUp用來為測試準(zhǔn)備環(huán)境,tearDown用來清理環(huán)境,已備之后的測試。

我們再執(zhí)行一次:

test_add (test_mathfunc.TestMathFunc)
Test method add(a, b) ... ok
test_divide (test_mathfunc.TestMathFunc)
Test method divide(a, b) ... FAIL
test_minus (test_mathfunc.TestMathFunc)
Test method minus(a, b) ... ok
test_multi (test_mathfunc.TestMathFunc)
Test method multi(a, b) ... ok

======================================================================
FAIL: test_divide (test_mathfunc.TestMathFunc)
Test method divide(a, b)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\py\test_mathfunc.py", line 36, in test_divide
    self.assertEqual(2.5, divide(5, 2))
AssertionError: 2.5 != 2

----------------------------------------------------------------------
Ran 4 tests in 0.000s

FAILED (failures=1)
do something before test.Prepare environment.
add
do something after test.Clean up.
do something before test.Prepare environment.
divide
do something after test.Clean up.
do something before test.Prepare environment.
minus
do something after test.Clean up.
do something before test.Prepare environment.
multi
do something after test.Clean up.

可以看到setUp和tearDown在每次執(zhí)行case前后都執(zhí)行了一次。

如果想要在所有case執(zhí)行之前準(zhǔn)備一次環(huán)境,并在所有case執(zhí)行結(jié)束之后再清理環(huán)境,我們可以用 setUpClass()tearDownClass():

...

class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py"""

    @classmethod
    def setUpClass(cls):
        print "This setUpClass() method only called once."

    @classmethod
    def tearDownClass(cls):
        print "This tearDownClass() method only called once too."

...

執(zhí)行結(jié)果如下:

...
This setUpClass() method only called once.
do something before test.Prepare environment.
add
do something after test.Clean up.
...
do something before test.Prepare environment.
multi
do something after test.Clean up.
This tearDownClass() method only called once too.

可以看到setUpClass以及tearDownClass均只執(zhí)行了一次。

跳過某個case

如果我們臨時想要跳過某個case不執(zhí)行怎么辦?unittest也提供了幾種方法:

  1. skip裝飾器
...

class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py"""

    ...

    @unittest.skip("I don't want to run this case.")
    def test_divide(self):
        """Test method divide(a, b)"""
        print "divide"
        self.assertEqual(2, divide(6, 3))
        self.assertEqual(2.5, divide(5, 2))

執(zhí)行:

...
test_add (test_mathfunc.TestMathFunc)
Test method add(a, b) ... ok
test_divide (test_mathfunc.TestMathFunc)
Test method divide(a, b) ... skipped "I don't want to run this case."
test_minus (test_mathfunc.TestMathFunc)
Test method minus(a, b) ... ok
test_multi (test_mathfunc.TestMathFunc)
Test method multi(a, b) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK (skipped=1)

可以看到總的test數(shù)量還是4個,但divide()方法被skip了。

skip裝飾器一共有三個 unittest.skip(reason)、unittest.skipIf(condition, reason)unittest.skipUnless(condition, reason),skip無條件跳過,skipIf當(dāng)condition為True時跳過,skipUnless當(dāng)condition為False時跳過。

  1. TestCase.skipTest()方法
...

class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py"""

    ...

    def test_divide(self):
        """Test method divide(a, b)"""
        self.skipTest('Do not run this.')
        print "divide"
        self.assertEqual(2, divide(6, 3))
        self.assertEqual(2.5, divide(5, 2))

輸出:

...
test_add (test_mathfunc.TestMathFunc)
Test method add(a, b) ... ok
test_divide (test_mathfunc.TestMathFunc)
Test method divide(a, b) ... skipped 'Do not run this.'
test_minus (test_mathfunc.TestMathFunc)
Test method minus(a, b) ... ok
test_multi (test_mathfunc.TestMathFunc)
Test method multi(a, b) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK (skipped=1)

效果跟上面的裝飾器一樣,跳過了divide方法。

進階——用HTMLTestRunner輸出漂亮的HTML報告

我們能夠輸出txt格式的文本執(zhí)行報告了,但是文本報告太過簡陋,是不是想要更加高大上的HTML報告?但unittest自己可沒有帶HTML報告,我們只能求助于外部的庫了。

HTMLTestRunner是一個第三方的unittest HTML報告庫,首先我們下載HTMLTestRunner.py,并放到當(dāng)前目錄下,或者你的'C:\Python27\Lib'下,就可以導(dǎo)入運行了。

下載地址:

官方原版:http://tungwaiyip.info/software/HTMLTestRunner.html

灰藍(lán)修改版:HTMLTestRunner.py(已調(diào)整格式,中文顯示)

修改我們的 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,
                                title='MathFunc Test Report',
                                description='generated by HTMLTestRunner.',
                                verbosity=2
                                )
        runner.run(suite)

這樣,在執(zhí)行時,在控制臺我們能夠看到執(zhí)行情況,如下:

ok test_add (test_mathfunc.TestMathFunc)
F  test_divide (test_mathfunc.TestMathFunc)
ok test_minus (test_mathfunc.TestMathFunc)
ok test_multi (test_mathfunc.TestMathFunc)

Time Elapsed: 0:00:00.001000

并且輸出了HTML測試報告,HTMLReport.html,如圖:

html report

這下漂亮的HTML報告也有了。其實你能發(fā)現(xiàn),HTMLTestRunner的執(zhí)行方法跟TextTestRunner很相似,你可以跟我上面的示例對比一下,就是把類圖中的runner換成了HTMLTestRunner,并將TestResult用HTML的形式展現(xiàn)出來,如果你研究夠深,可以寫自己的runner,生成更復(fù)雜更漂亮的報告。

總結(jié)一下

  1. unittest是Python自帶的單元測試框架,我們可以用其來作為我們自動化測試框架的用例組織執(zhí)行框架。
  2. unittest的流程:寫好TestCase,然后由TestLoader加載TestCase到TestSuite,然后由TextTestRunner來運行TestSuite,運行的結(jié)果保存在TextTestResult中,我們通過命令行或者unittest.main()執(zhí)行時,main會調(diào)用TextTestRunner中的run來執(zhí)行,或者我們可以直接通過TextTestRunner來執(zhí)行用例。
  3. 一個class繼承unittest.TestCase即是一個TestCase,其中以 test 開頭的方法在load時被加載為一個真正的TestCase。
  4. verbosity參數(shù)可以控制執(zhí)行結(jié)果的輸出,0 是簡單報告、1 是一般報告、2 是詳細(xì)報告。
  5. 可以通過addTest和addTests向suite中添加case或suite,可以用TestLoader的loadTestsFrom__()方法。
  6. setUp()、tearDown()、setUpClass()以及 tearDownClass()可以在用例執(zhí)行前布置環(huán)境,以及在用例執(zhí)行后清理環(huán)境
  7. 我們可以通過skip,skipIf,skipUnless裝飾器跳過某個case,或者用TestCase.skipTest方法。
  8. 參數(shù)中加stream,可以將報告輸出到文件:可以用TextTestRunner輸出txt報告,以及可以用HTMLTestRunner輸出html報告。

我們這里沒有討論命令行的使用以及模塊級別的fixture,感興趣的同學(xué)可以自行搜索資料學(xué)習(xí)。


更多關(guān)于python selenium的文章,請關(guān)注我的CSDN專欄:Python Selenium自動化測試詳解

最后,博主最近新建了一個QQ群,用于Python Selenium的技術(shù)交流,不討論其他技術(shù),不討論其他語言,專注于Python+Selenium的技術(shù)交流與分享,感興趣的同學(xué)可以加群(455478219),歡迎來跟博主討論Python Selenium有關(guān)的問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • unittest作為一個python中的基本模塊,是其他框架和工具的基礎(chǔ),官方文檔神馬的最實用了:https://...
    cheneydc閱讀 8,234評論 0 3
  • Unittest核心工作原理 最核心的四個概念:test case ,test suite,test runner...
    簡單生活101閱讀 1,866評論 0 1
  • Startup 單元測試的核心價值在于兩點: 更加精確地定義某段代碼的作用,從而使代碼的耦合性更低 避免程序員寫出...
    wuwenxiang閱讀 10,234評論 1 27
  • 因為unittest支持的html報告在作為郵件附加時耗時較長,故將報告擴展支持為unishark框架。 基于un...
    五娃兒閱讀 602評論 0 0
  • 有一天晚上躺在沙發(fā)上看完一部虐狗電影,再看看時間——9點14分。那一刻,沒有睡意;那一刻,孤獨到死。 沒有人與我...
    Ms_Who閱讀 203評論 0 0

友情鏈接更多精彩內(nèi)容