每周一個 Python 模塊 | unittest

unittest 是 Python 自帶的單元測試框架姑廉,可以用來作自動化測試框架的用例組織執(zhí)行缺亮。

優(yōu)點:提供用例組織與執(zhí)行方法;提供比較方法桥言;提供豐富的日志萌踱、清晰的報告。

unittest 核心工作原理

unittest 中最核心的部分是:TestFixture号阿、TestCase并鸵、TestSuite、TestRunner扔涧。

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

  • 一個 TestCase 的實例就是一個測試用例园担。什么是測試用例呢?就是一個完整的測試流程枯夜,包括測試前準備環(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é)果會保存到 TextTestResul t實例中,包括運行了多少測試用例桑逝,成功了多少,失敗了多少等信息俏让。
  • 而對一個測試用例環(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í)行時,默認將執(zhí)行結(jié)果輸出到控制臺远寸,我們可以設(shè)置其輸出到文件抄淑,在文件中查看結(jié)果(你可能聽說過 HTMLTestRunner,是的驰后,通過它可以將結(jié)果輸出到 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()

執(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é)果的標識贱呐,成功是 .,失敗是 F入桂,出錯是 E奄薇,跳過是 S。從上面也可以看出事格,測試的執(zhí)行跟方法的順序沒有關(guān)系惕艳,test_divide 寫在了第 4 個,但是卻是第 2 個執(zhí)行的驹愚。
  2. 每個測試方法均以 test 開頭远搪,否則是不被 unittest 識別的。
  3. unittest.main() 中加 verbosity 參數(shù)可以控制輸出的錯誤報告的詳細程度逢捺,默認是 1谁鳍,如果設(shè)為 0,則不輸出每一用例的執(zhí)行結(jié)果劫瞳,即沒有上面的結(jié)果中的第 1 行倘潜;如果設(shè)為 2,則輸出詳細的執(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)

可以看到涮因,每一個用例的詳細執(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。

TestLoader 并輸出結(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 格式的測試報告了屈嗤。

但是文本報告太過簡陋潘拨,是不是想要更加高大上的 HTML 報告?但 unittest 自己可沒有帶 HTML 報告饶号,我們只能求助于外部的庫了铁追。

HTMLTestRunner 是一個第三方的 unittest HTML 報告庫,我們下載 HTMLTestRunner.py茫船,并導(dǎo)入就可以運行了琅束。

官方地址:http://tungwaiyip.info/software/HTMLTestRunner.html

修改我們的 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 報告也有了艾船。其實你能發(fā)現(xiàn)葵腹,HTMLTestRunner 的執(zhí)行方法跟 TextTestRunner 很相似,你可以跟上面的示例對比一下屿岂,就是把類圖中的 runner 換成了 HTMLTestRunner践宴,并將 TestResult 用 HTML 的形式展現(xiàn)出來,如果你研究夠深爷怀,可以寫自己的 runner阻肩,生成更復(fù)雜更漂亮的報告。

TestFixture 準備和清除環(huán)境

上面整個測試基本跑了下來运授,但可能會遇到點特殊的情況:如果我的測試需要在每次執(zhí)行之前準備環(huán)境烤惊,或者在每次執(zhí)行完之后需要進行一些清理怎么辦?比如執(zhí)行前需要連接數(shù)據(jù)庫吁朦,執(zhí)行完成之后需要還原數(shù)據(jù)柒室、斷開連接±辏總不能每個測試方法中都添加準備環(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 用來為測試準備環(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í)行之前準備一次環(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
...
multi
do something after test.Clean up.
This tearDownClass() method only called once too.

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

一些有用的方法

斷言 Assert

大多數(shù)測試斷言某些條件的真實性亿昏。編寫真值檢查測試有兩種不同的方法,具體取決于測試作者的觀點以及所測試代碼的預(yù)期結(jié)果档礁。

# unittest_truth.py

import unittest


class TruthTest(unittest.TestCase):

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

    def testAssertFalse(self):
        self.assertFalse(False)

如果代碼生成的值為 true角钩,則應(yīng)使用assertTrue()方法。如果代碼產(chǎn)生值為 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)

當(dāng)失敗時惨险,這些特殊的測試方法會產(chǎn)生錯誤消息,包括被比較的值脊髓。

$ 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()測試浮點數(shù)的近似相等。

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

參數(shù)是要比較的值将硝,以及用于測試的小數(shù)位數(shù)恭朗。

$ 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(),也有比較list袋哼,dictset 對象的方法冀墨。

# 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'),
        )

每種方法都使用對輸入類型有意義的格式定義函數(shù),使測試失敗更容易理解和糾正涛贯。

$ 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()測試容器關(guān)系。

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

測試異常

如前所述弟翘,如果測試引發(fā)異常,則將 AssertionError視為錯誤骄酗。這對于修改具有現(xiàn)有測試覆蓋率的代碼時發(fā)現(xiàn)錯誤非常有用稀余。但是,在某些情況下趋翻,測試應(yīng)驗證某些代碼是否確實產(chǎn)生異常睛琳。

例如,如果給對象的屬性賦予無效值踏烙。在這種情況下师骗, 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',
        )

兩者的結(jié)果是相同的讨惩,但第二次使用的 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

用不同的輸入重復(fù)測試

使用不同的輸入運行相同的測試邏輯通常很有用。不是為每個小案例定義單獨的測試方法荐捻,而是使用一種包含多個相關(guān)斷言調(diào)用的測試方法黍少。這種方法的問題在于,只要一個斷言失敗处面,就會跳過其余的斷言厂置。更好的解決方案是使用subTest()在測試方法中為測試創(chuàng)建上下文。如果測試失敗魂角,則報告失敗并繼續(xù)進行其余測試昵济。

# 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 不執(zhí)行怎么辦沼溜?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))

執(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 時跳過廊酣。

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)

如果預(yù)期失敗的測試通過了晓猛,則該條件被視為特殊類型的失敗,并報告為“意外成功”凡辱。

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

總結(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 是詳細報告膀斋。
  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 報告败砂。

相關(guān)文檔:

https://pymotw.com/3/unittest/index.html

https://huilansame.github.io/huilansame.github.io/archivers/python-unittest

https://segmentfault.com/a/1190000016315201#articleHeader0

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赌渣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子昌犹,更是在濱河造成了極大的恐慌坚芜,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件斜姥,死亡現(xiàn)場離奇詭異鸿竖,居然都是意外死亡,警方通過查閱死者的電腦和手機疾渴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來搞坝,“玉大人,你說我怎么就攤上這事桩撮『当” “怎么了房待?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵流椒,是天一觀的道長极谊。 經(jīng)常有香客問我咙边,道長,這世上最難降的妖魔是什么仓犬? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任制圈,我火速辦了婚禮们童,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鲸鹦。我一直安慰自己慧库,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布馋嗜。 她就那樣靜靜地躺著齐板,像睡著了一般。 火紅的嫁衣襯著肌膚如雪葛菇。 梳的紋絲不亂的頭發(fā)上甘磨,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機與錄音眯停,去河邊找鬼济舆。 笑死,一個胖子當(dāng)著我的面吹牛莺债,可吹牛的內(nèi)容都是我干的滋觉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼齐邦,長吁一口氣:“原來是場噩夢啊……” “哼椎侠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起措拇,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤我纪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后丐吓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宣羊,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年汰蜘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片之宿。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡族操,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出比被,到底是詐尸還是另有隱情色难,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布等缀,位于F島的核電站枷莉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏尺迂。R本人自食惡果不足惜笤妙,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一冒掌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蹲盘,春花似錦股毫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至苍凛,卻和暖如春趣席,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背醇蝴。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工宣肚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人哑蔫。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓钉寝,卻偏偏與公主長得像,于是被迫代替她去往敵國和親闸迷。 傳聞我的和親對象是個殘疾皇子嵌纲,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容