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 方法是有問題的。
這就是一個簡單的測試涣澡,有幾點需要說明的:
- 在第一行給出了每一個用例執(zhí)行的結(jié)果的標識贱呐,成功是
.
,失敗是F
入桂,出錯是E
奄薇,跳過是S
。從上面也可以看出事格,測試的執(zhí)行跟方法的順序沒有關(guān)系惕艳,test_divide 寫在了第 4 個,但是卻是第 2 個執(zhí)行的驹愚。 - 每個測試方法均以
test
開頭远搪,否則是不被 unittest 識別的。 - 在
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
袋哼,dict
和set
對象的方法冀墨。
# 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é)
- unittest 是 Python 自帶的單元測試框架戒职,我們可以用其來作為我們自動化測試框架的用例組織執(zhí)行框架。
- unittest 的流程:寫好 TestCase透乾,然后由 TestLoader 加載 TestCase 到 TestSuite洪燥,然后由 TextTestRunner來運行 TestSuite,運行的結(jié)果保存在 TextTestResult 中乳乌,我們通過命令行或者 unittest.main() 執(zhí)行時捧韵,main 會調(diào)用 TextTestRunner 中的 run 來執(zhí)行,或者我們可以直接通過 TextTestRunner 來執(zhí)行用例钦扭。
- 一個 class 繼承 unittest.TestCase 即是一個 TestCase纫版,其中以
test
開頭的方法在 load 時被加載為一個真正的 TestCase。 - verbosity 參數(shù)可以控制執(zhí)行結(jié)果的輸出客情,
0
是簡單報告其弊、1
是一般報告、2
是詳細報告膀斋。 - 可以通過 addTest 和 addTests 向 suite 中添加 case 或 suite梭伐,可以用 TestLoader 的 loadTestsFrom__() 方法。
- 用
setUp()
仰担、tearDown()
糊识、setUpClass()
以及tearDownClass()
可以在用例執(zhí)行前布置環(huán)境,以及在用例執(zhí)行后清理環(huán)境。 - 我們可以通過 skip赂苗,skipIf愉耙,skipUnless 裝飾器跳過某個 case,或者用 TestCase.skipTest 方法拌滋。
- 參數(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