每週一個 Python 模組 | unittest

yongxinz發表於2018-11-28

專欄地址:每週一個 Python 模組

unittest 是 Python 自帶的單元測試框架,可以用來作自動化測試框架的用例組織執行。

優點:提供用例組織與執行方法;提供比較方法;提供豐富的日誌、清晰的報告。

unittest 核心工作原理

unittest 中最核心的部分是:TestFixture、TestCase、TestSuite、TestRunner。

下面我們分別來解釋這四個概念的意思:

  • 一個 TestCase 的例項就是一個測試用例。什麼是測試用例呢?就是一個完整的測試流程,包括測試前準備環境的搭建(setUp),執行測試程式碼(run),以及測試後環境的還原(tearDown)。元測試(unit test)的本質也就在這裡,一個測試用例是一個完整的測試單元,通過執行這個測試單元,可以對某一個問題進行驗證。
  • 而多個測試用例集合在一起,就是 TestSuite,而且 TestSuite 也可以巢狀 TestSuite。
  • TestLoader 是用來載入 TestCase 到 TestSuite 中的,其中有幾個 loadTestsFrom__() 方法,就是從各個地方尋找 TestCase,建立它們的例項,然後 add 到 TestSuite 中,再返回一個 TestSuite 例項。
  • TextTestRunner 是來執行測試用例的,其中的 run(test) 會執行 TestSuite/TestCase 中的 run(result) 方法。 測試的結果會儲存到 TextTestResul t例項中,包括執行了多少測試用例,成功了多少,失敗了多少等資訊。
  • 而對一個測試用例環境的搭建和銷燬,是一個 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。

寫 TestCase

先準備待測試的方法,如下:

# mathfunc.py

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

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
複製程式碼

寫 TestCase,如下:

# 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。

TestLoader 並輸出結果

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

修改 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 格式的測試報告了。

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

HTMLTestRunner 是一個第三方的 unittest HTML 報告庫,我們下載 HTMLTestRunner.py,並匯入就可以執行了。

官方地址:tungwaiyip.info/software/HT…

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

TestFixture 準備和清除環境

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

這就要涉及到我們之前說過的 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
...
multi
do something after test.Clean up.
This tearDownClass() method only called once too.
複製程式碼

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

一些有用的方法

斷言 Assert

大多數測試斷言某些條件的真實性。編寫真值檢查測試有兩種不同的方法,具體取決於測試作者的觀點以及所測試程式碼的預期結果。

# unittest_truth.py

import unittest


class TruthTest(unittest.TestCase):

    def testAssertTrue(self):
        self.assertTrue(True)

    def testAssertFalse(self):
        self.assertFalse(False)
複製程式碼

如果程式碼生成的值為 true,則應使用assertTrue()方法。如果程式碼產生值為 false,則方法assertFalse()更有意義。

$ python3 -m unittest -v unittest_truth.py

testAssertFalse (unittest_truth.TruthTest) ... ok
testAssertTrue (unittest_truth.TruthTest) ... ok

----------------------------------------------------------------
Ran 2 tests in 0.000s

OK
複製程式碼

測試相等

unittest包括測試兩個值相等的方法如下:

# unittest_equality.py 

import unittest


class EqualityTest(unittest.TestCase):

    def testExpectEqual(self):
        self.assertEqual(1, 3 - 2)

    def testExpectEqualFails(self):
        self.assertEqual(2, 3 - 2)

    def testExpectNotEqual(self):
        self.assertNotEqual(2, 3 - 2)

    def testExpectNotEqualFails(self):
        self.assertNotEqual(1, 3 - 2)
複製程式碼

當失敗時,這些特殊的測試方法會產生錯誤訊息,包括被比較的值。

$ python3 -m unittest -v unittest_equality.py

testExpectEqual (unittest_equality.EqualityTest) ... ok
testExpectEqualFails (unittest_equality.EqualityTest) ... FAIL
testExpectNotEqual (unittest_equality.EqualityTest) ... ok
testExpectNotEqualFails (unittest_equality.EqualityTest) ... FAIL

================================================================
FAIL: testExpectEqualFails (unittest_equality.EqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_equality.py", line 15, in
testExpectEqualFails
    self.assertEqual(2, 3 - 2)
AssertionError: 2 != 1

================================================================
FAIL: testExpectNotEqualFails (unittest_equality.EqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_equality.py", line 21, in
testExpectNotEqualFails
    self.assertNotEqual(1, 3 - 2)
AssertionError: 1 == 1

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

FAILED (failures=2)
複製程式碼

幾乎相等

除了嚴格相等之外,還可以使用assertAlmostEqual()assertNotAlmostEqual()測試浮點數的近似相等。

# unittest_almostequal.py 

import unittest


class AlmostEqualTest(unittest.TestCase):

    def testEqual(self):
        self.assertEqual(1.1, 3.3 - 2.2)

    def testAlmostEqual(self):
        self.assertAlmostEqual(1.1, 3.3 - 2.2, places=1)

    def testNotAlmostEqual(self):
        self.assertNotAlmostEqual(1.1, 3.3 - 2.0, places=1)
複製程式碼

引數是要比較的值,以及用於測試的小數位數。

$ python3 -m unittest unittest_almostequal.py

.F.
================================================================
FAIL: testEqual (unittest_almostequal.AlmostEqualTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_almostequal.py", line 12, in testEqual
    self.assertEqual(1.1, 3.3 - 2.2)
AssertionError: 1.1 != 1.0999999999999996

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

FAILED (failures=1)
複製程式碼

容器

除了通用的assertEqual()assertNotEqual(),也有比較listdictset 物件的方法。

# unittest_equality_container.py 

import textwrap
import unittest


class ContainerEqualityTest(unittest.TestCase):

    def testCount(self):
        self.assertCountEqual(
            [1, 2, 3, 2],
            [1, 3, 2, 3],
        )

    def testDict(self):
        self.assertDictEqual(
            {'a': 1, 'b': 2},
            {'a': 1, 'b': 3},
        )

    def testList(self):
        self.assertListEqual(
            [1, 2, 3],
            [1, 3, 2],
        )

    def testMultiLineString(self):
        self.assertMultiLineEqual(
            textwrap.dedent("""
            This string
            has more than one
            line.
            """),
            textwrap.dedent("""
            This string has
            more than two
            lines.
            """),
        )

    def testSequence(self):
        self.assertSequenceEqual(
            [1, 2, 3],
            [1, 3, 2],
        )

    def testSet(self):
        self.assertSetEqual(
            set([1, 2, 3]),
            set([1, 3, 2, 4]),
        )

    def testTuple(self):
        self.assertTupleEqual(
            (1, 'a'),
            (1, 'b'),
        )
複製程式碼

每種方法都使用對輸入型別有意義的格式定義函式,使測試失敗更容易理解和糾正。

$ python3 -m unittest unittest_equality_container.py

FFFFFFF
================================================================
FAIL: testCount
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_equality_container.py", line 15, in
testCount
    [1, 3, 2, 3],
AssertionError: Element counts were not equal:
First has 2, Second has 1:  2
First has 1, Second has 2:  3

================================================================
FAIL: testDict
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_equality_container.py", line 21, in
testDict
    {'a': 1, 'b': 3},
AssertionError: {'a': 1, 'b': 2} != {'a': 1, 'b': 3}
- {'a': 1, 'b': 2}
?               ^

+ {'a': 1, 'b': 3}
?               ^


================================================================
FAIL: testList
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_equality_container.py", line 27, in
testList
    [1, 3, 2],
AssertionError: Lists differ: [1, 2, 3] != [1, 3, 2]

First differing element 1:
2
3

- [1, 2, 3]
+ [1, 3, 2]

================================================================
FAIL: testMultiLineString
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_equality_container.py", line 41, in
testMultiLineString
    """),
AssertionError: '\nThis string\nhas more than one\nline.\n' !=
'\nThis string has\nmore than two\nlines.\n'

- This string
+ This string has
?            ++++
- has more than one
? ----           --
+ more than two
?           ++
- line.
+ lines.
?     +


================================================================
FAIL: testSequence
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_equality_container.py", line 47, in
testSequence
    [1, 3, 2],
AssertionError: Sequences differ: [1, 2, 3] != [1, 3, 2]

First differing element 1:
2
3

- [1, 2, 3]
+ [1, 3, 2]

================================================================
FAIL: testSet
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_equality_container.py", line 53, in testSet
    set([1, 3, 2, 4]),
AssertionError: Items in the second set but not the first:
4

================================================================
FAIL: testTuple
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_equality_container.py", line 59, in
testTuple
    (1, 'b'),
AssertionError: Tuples differ: (1, 'a') != (1, 'b')

First differing element 1:
'a'
'b'

- (1, 'a')
?      ^

+ (1, 'b')
?      ^


----------------------------------------------------------------
Ran 7 tests in 0.005s

FAILED (failures=7)
複製程式碼

使用assertIn()測試容器關係。

# unittest_in.py 

import unittest


class ContainerMembershipTest(unittest.TestCase):

    def testDict(self):
        self.assertIn(4, {1: 'a', 2: 'b', 3: 'c'})

    def testList(self):
        self.assertIn(4, [1, 2, 3])

    def testSet(self):
        self.assertIn(4, set([1, 2, 3]))
複製程式碼

任何物件都支援in運算子或容器 API assertIn()

$ python3 -m unittest unittest_in.py

FFF
================================================================
FAIL: testDict (unittest_in.ContainerMembershipTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_in.py", line 12, in testDict
    self.assertIn(4, {1: 'a', 2: 'b', 3: 'c'})
AssertionError: 4 not found in {1: 'a', 2: 'b', 3: 'c'}

================================================================
FAIL: testList (unittest_in.ContainerMembershipTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_in.py", line 15, in testList
    self.assertIn(4, [1, 2, 3])
AssertionError: 4 not found in [1, 2, 3]

================================================================
FAIL: testSet (unittest_in.ContainerMembershipTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_in.py", line 18, in testSet
    self.assertIn(4, set([1, 2, 3]))
AssertionError: 4 not found in {1, 2, 3}

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

FAILED (failures=3)
複製程式碼

測試異常

如前所述,如果測試引發異常,則將 AssertionError視為錯誤。這對於修改具有現有測試覆蓋率的程式碼時發現錯誤非常有用。但是,在某些情況下,測試應驗證某些程式碼是否確實產生異常。

例如,如果給物件的屬性賦予無效值。在這種情況下, assertRaises()使程式碼比在測試中捕獲異常更清晰。比較這兩個測試:

# unittest_exception.py 

import unittest


def raises_error(*args, **kwds):
    raise ValueError('Invalid value: ' + str(args) + str(kwds))


class ExceptionTest(unittest.TestCase):

    def testTrapLocally(self):
        try:
            raises_error('a', b='c')
        except ValueError:
            pass
        else:
            self.fail('Did not see ValueError')

    def testAssertRaises(self):
        self.assertRaises(
            ValueError,
            raises_error,
            'a',
            b='c',
        )
複製程式碼

兩者的結果是相同的,但第二次使用的 assertRaises()更簡潔。

$ python3 -m unittest -v unittest_exception.py

testAssertRaises (unittest_exception.ExceptionTest) ... ok
testTrapLocally (unittest_exception.ExceptionTest) ... ok

----------------------------------------------------------------
Ran 2 tests in 0.000s

OK
複製程式碼

用不同的輸入重複測試

使用不同的輸入執行相同的測試邏輯通常很有用。不是為每個小案例定義單獨的測試方法,而是使用一種包含多個相關斷言呼叫的測試方法。這種方法的問題在於,只要一個斷言失敗,就會跳過其餘的斷言。更好的解決方案是使用subTest()在測試方法中為測試建立上下文。如果測試失敗,則報告失敗並繼續進行其餘測試。

# unittest_subtest.py 

import unittest


class SubTest(unittest.TestCase):

    def test_combined(self):
        self.assertRegex('abc', 'a')
        self.assertRegex('abc', 'B')
        # The next assertions are not verified!
        self.assertRegex('abc', 'c')
        self.assertRegex('abc', 'd')

    def test_with_subtest(self):
        for pat in ['a', 'B', 'c', 'd']:
            with self.subTest(pattern=pat):
                self.assertRegex('abc', pat)
複製程式碼

在該示例中,test_combined()方法從不執行斷言'c''d'test_with_subtest()方法可以正確報告其他故障。請注意,即使報告了三個故障,測試執行器仍然認為只有兩個測試用例。

$ python3 -m unittest -v unittest_subtest.py

test_combined (unittest_subtest.SubTest) ... FAIL
test_with_subtest (unittest_subtest.SubTest) ...
================================================================
FAIL: test_combined (unittest_subtest.SubTest)
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_subtest.py", line 13, in test_combined
    self.assertRegex('abc', 'B')
AssertionError: Regex didn't match: 'B' not found in 'abc'

================================================================
FAIL: test_with_subtest (unittest_subtest.SubTest) (pattern='B')
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_subtest.py", line 21, in test_with_subtest
    self.assertRegex('abc', pat)
AssertionError: Regex didn't match: 'B' not found in 'abc'

================================================================
FAIL: test_with_subtest (unittest_subtest.SubTest) (pattern='d')
----------------------------------------------------------------
Traceback (most recent call last):
  File ".../unittest_subtest.py", line 21, in test_with_subtest
    self.assertRegex('abc', pat)
AssertionError: Regex didn't match: 'd' not found in 'abc'

----------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=3)
複製程式碼

跳過某個 case

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

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 時跳過。

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 方法。

忽略失敗的測試

可以使用expectedFailure()裝飾器來忽略失敗的測試。

# unittest_expectedfailure.py 

import unittest


class Test(unittest.TestCase):

    @unittest.expectedFailure
    def test_never_passes(self):
        self.assertTrue(False)

    @unittest.expectedFailure
    def test_always_passes(self):
        self.assertTrue(True)
複製程式碼

如果預期失敗的測試通過了,則該條件被視為特殊型別的失敗,並報告為“意外成功”。

$ python3 -m unittest -v unittest_expectedfailure.py

test_always_passes (unittest_expectedfailure.Test) ...
unexpected success
test_never_passes (unittest_expectedfailure.Test) ... expected
failure

----------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (expected failures=1, unexpected successes=1)
複製程式碼

總結

  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 報告。



相關文件:

pymotw.com/3/unittest/…

huilansame.github.io/huilansame.…

segmentfault.com/a/119000001…

相關文章