學習筆記之Python必會的單元測試框架unittest

原文地址:https://blog.csdn.net/huilan_same/article/details/52944782

文章詳細說明unittest模塊用法,內容豐富,詳細,贊。

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

unittest核心工作原理

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

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

unittest類圖

  • 一個TestCase的實例就是一個測試用例。什麼是測試用例呢?就是一個完整的測試流程,包括測試前準備環境的搭建(setUp),執行測試代碼(run),以及測試後環境的還原(tearDown)。元測試(unit test)的本質也就在這裏,一個測試用例是一個完整的測試單元,通過運行這個測試單元,可以對某一個問題進行驗證。

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

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

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

  • 而對一個測試用例環境的搭建和銷燬,是一個fixture。

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

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

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

unittest實例

下面我們通過一些實例來更好地認識一下unittest。

我們先來準備一些待測方法:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

簡單示例

接下來我們爲這些方法寫一個測試:

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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

執行結果:

.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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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

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

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

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

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

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

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

組織TestSuite

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

問題二是我們現在只有一個測試文件,我們直接執行該文件即可,但如果有多個測試文件,怎麼進行組織,總不能一個個文件執行吧,答案也在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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

執行結果:

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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

上面用了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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注意,用TestLoader的方法是無法對case進行排序的,同時,suite中也可以套suite。

將結果輸出到文件中

用例組織好了,但結果只能輸出到控制檯,這樣沒有辦法查看之前的執行記錄,我們想將結果輸出到文件。很簡單,看示例:

修改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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

執行此文件,可以看到,在同目錄下生成了UnittestTextReport.txt,所有的執行報告均輸出到了此文件中,這下我們便有了txt格式的測試報告了。

test fixture之setUp() tearDown()

上面整個測試基本跑了下來,但可能會遇到點特殊的情況:如果我的測試需要在每次執行之前準備環境,或者在每次執行完之後需要進行一些清理怎麼辦?比如執行前需要連接數據庫,執行完成之後需要還原數據、斷開連接。總不能每個測試方法中都添加準備環境、清理環境的代碼吧。

這就要涉及到我們之前說過的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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

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

我們再執行一次:

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.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

可以看到setUp和tearDown在每次執行case前後都執行了一次。

如果想要在所有case執行之前準備一次環境,並在所有case執行結束之後再清理環境,我們可以用 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."

...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

執行結果如下:

...
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.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以看到setUpClass以及tearDownClass均只執行了一次。

跳過某個case

如果我們臨時想要跳過某個case不執行怎麼辦?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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

執行:

...
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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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

skip裝飾器一共有三個 unittest.skip(reason)unittest.skipIf(condition, reason)unittest.skipUnless(condition, reason),skip無條件跳過,skipIf當condition爲True時跳過,skipUnless當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))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

輸出:

...
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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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

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

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

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

下載地址:

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

灰藍修改版:HTMLTestRunner.py(已調整格式,中文顯示)

修改我們的 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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

這樣,在執行時,在控制檯我們能夠看到執行情況,如下:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

html report

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

總結一下:

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

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章