1. 程式人生 > >【單元測試】Python UNITTEST

【單元測試】Python 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

簡單示例

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

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

執行結果:

.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. 在第一行給出了每一個用例執行的結果的標識,成功是 .,失敗是 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)

可以看到,每一個用例的詳細執行情況以及用例名,用例描述均被輸出了出來(在測試方法下加程式碼示例中的”“”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)

執行結果:

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)

可以看到,執行情況跟我們預料的一樣:執行了三個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))

注意,用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)

執行此檔案,可以看到,在同目錄下生成了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))

我們添加了 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.

可以看到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."

...

執行結果如下:

...
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均只執行了一次。

跳過某個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))

執行:

...
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數量還是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))

輸出:

...
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格式的文字執行報告了,但是文字報告太過簡陋,是不是想要更加高大上的HTML報告?但unittest自己可沒有帶HTML報告,我們只能求助於外部的庫了。

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

下載地址:

修改我們的 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)

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

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報告也有了。其實你能發現,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,感興趣的同學可以自行搜尋資料學習。