每週一個 Python 模塊 | unittest

專欄地址:每週一個 Python 模塊html

unittest 是 Python 自帶的單元測試框架,能夠用來做自動化測試框架的用例組織執行。python

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

unittest 核心工做原理

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

下面咱們分別來解釋這四個概念的意思:數據庫

  • 一個 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 中時也有四個測試用例。segmentfault

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

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

unittest 實例

下面咱們經過一些實例來更好地認識一下 unittest。框架

寫 TestCase

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

# 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,以下:ide

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

相關文章
相關標籤/搜索