簡介
unittest是Python的內(nèi)建模塊,是Python單元測試的事實標(biāo)準(zhǔn),也叫PyUnit。使用unittest之前,先了解如下幾個概念:
- test case:測試用例,可以通過創(chuàng)建
unitest.TestCase類的子類創(chuàng)建一個測試用例。 - test fixture:包含執(zhí)行測試用例前的測試準(zhǔn)備工作、測試用例執(zhí)行后的清理工作(分別對應(yīng)
TestCase中的setUp()和tearDown()方法),測試準(zhǔn)備和測試清理的目的是保證每個測試用例執(zhí)行前后的系統(tǒng)狀態(tài)一致。 - test suite:測試套,是測試用例、測試套或者兩者的集合,用來將有關(guān)聯(lián)的測試項打包。
- test runner:負責(zé)執(zhí)行測試并將結(jié)果展示給用戶,可以展示圖形或文字形式(
unittest.TextTestRunner)的結(jié)果,或者返回一個錯誤碼標(biāo)識測試用例的執(zhí)行結(jié)果。test runner提供了一個方法run(),接受一個unittest.TestSuite或unittest.TestCase實例作為參數(shù),執(zhí)行對應(yīng)測試項目后返回測試結(jié)果unittest.TestResult對象。
基本使用方法
定義測試用例的方法如下:
#unit.py
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('Loo'.upper(), 'LOO')
def test_isupper(self):
self.assertTrue('LOO'.isupper())
self.assertFalse('Loo'.isupper())
def test_split(self):
s = 'Mars Loo'
with self.assertRaises(TypeError):
s.split(2)
if __name__ == '__main__':
unittest.main()
執(zhí)行腳本:
$ python unit.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
每一個測試項目的函數(shù)定義以test開頭命名,這樣test runner就知道哪些函數(shù)是應(yīng)該被執(zhí)行的。上面的例子展示了驗證測試結(jié)果常用的三種方法:
-
assertEqual(a, b):比較a==b。 -
assertTrue(exp)和assertFalse(exp):驗證bool(exp)為True或者False。 -
assertRaises(Exception):驗證Exception被拋出。
之所以不使用Python內(nèi)建的assert拋出異常,是因為test runner需要根據(jù)這些封裝后的方法拋出的異常做測試結(jié)果統(tǒng)計。
unittest.main()方法會在當(dāng)前模塊尋找所有unittest.TestCase的子類,并執(zhí)行它們中的所有測試項目。使用-v參數(shù)可以看到更詳細的測試執(zhí)行過程:
$ python unit.py -v
test_isupper (__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
也可以修改最后兩行成如下代碼:
suite = unittest.TestLoader().loadTestsFromTestCase(TestStringMethods)
unittest.TextTestRunner(verbosity=2).run(suite)
測試結(jié)果如下:
$ python unit.py
test_isupper (__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
從命令行運行unittest
$ python -m unittest unit #直接運行模塊unit中的測試用例
$ python -m unittest unit.TestStringMethods #運行模塊中的某個類
$ python -m unittest unit.TestStringMethods.test_upper #運行某個單獨的測試方法
混合運行測試模塊、類以及測試方法也是可以的。
如果要查看unittest模塊命令行的更多參數(shù)信息,使用-h參數(shù):
$ python -m unittest -h
-b參數(shù):只在測試用例fail或者error時顯示它的stdout和stderr,否則不會顯示。-f參數(shù):如果有一個測試用例fail或者出現(xiàn)error,立即停止測試。-c參數(shù):捕捉Control-C信號,并顯示測試結(jié)果。
自動發(fā)現(xiàn)測試用例
unittest能夠自動發(fā)現(xiàn)測試用例。為了讓測試用例能夠被自動發(fā)現(xiàn),測試文件需要是在項目目錄中可以import的module或者package,比如如下目錄結(jié)構(gòu):
unittest
├── test_a
│ ├── __init__.py
│ └── test_a.py
└── test_b.py
在unittest目錄中運行如下命令,即可運行test_a這個package和test_b這個module中的測試項目:
$ python -m unittest discover
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
unittest discovery默認(rèn)會搜索名字命名符合test*的module或package,可以添加更多的參數(shù):
-
-v:詳細輸出。 -
-s:開始自動搜索的目錄,默認(rèn)是.;這個參數(shù)也可以指向一個package名,而不是目錄,例如unittest.test_a。 -
-p:文件匹配的模式,默認(rèn)是test*.py。 -
-t:項目頂級目錄,默認(rèn)與開始自動搜索的目錄相同。
比如:
$ python -m unittest discover -s test_a
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
測試用例發(fā)現(xiàn)通過import模塊或package執(zhí)行測試,例如foo/bar/baz.py會被import為foo.bar.baz。
測試代碼的組織
測試用例一定要是自包含的,即測試用例既可以獨立運行,也可以和其他測試用例混合執(zhí)行,測試用例執(zhí)行前后不能影響系統(tǒng)狀態(tài)。
建議將被測試代碼和測試代碼分離,比如一個模塊module.py對應(yīng)的單元測試的文件是test_module.py,這樣方便維護。
最簡單的測試用例定義,是一個unittest.TestCase的子類只包含一個測試步驟,這個時候只需要定義一個runTest方法,比如:
# unit.py
import unittest
class MyTestCase(unittest.TestCase):
def runTest(self):
self.assertEqual(1, 2, 'not equal')
執(zhí)行測試后結(jié)果如下:
$ python -m unittest -v unit
runTest (unit.MyTestCase) ... FAIL
======================================================================
FAIL: runTest (unit.MyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "unit.py", line 5, in runTest
self.assertEqual(1, 2, 'not equal')
AssertionError: not equal
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
如果assert*方法檢查失敗,會拋出一個異常,unittest會將其算作失?。╢ailure)。任何其他異常都被unittest算作錯誤(error),比如:
#unit.py
import unittest
class MyTestCase(unittest.TestCase):
def runTest(self):
self.assertEqual(notexist, 2, 'not exist')
執(zhí)行測試結(jié)果如下:
$ python -m unittest -v unit
runTest (unit.MyTestCase) ... ERROR
======================================================================
ERROR: runTest (unit.MyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "unit.py", line 5, in runTest
self.assertEqual(notexist, 2, 'not exist')
NameError: global name 'notexist' is not defined
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
即failure通常是實際結(jié)果與預(yù)期結(jié)果不符,error通常是因為測試代碼有bug導(dǎo)致。
如果很多測試項目的初始化準(zhǔn)備工作類似,可以為他們定義同一個setUp方法,比如:
import unittest
class BaseTestCase(unittest.TestCase):
def setUp(self):
self._value = 12
class TestCase1(BaseTestCase):
def runTest(self):
self.assertEqual(self._value, 12, 'default value error')
class TestCase2(BaseTestCase):
def runTest(self):
self._value = 13
self.assertEqual(self._value, 13, 'change value fail')
如果基類BaseTestCase的setUp方法中拋出異常,unittest不會繼續(xù)執(zhí)行子類中的runTest方法。
如果想在測試項目執(zhí)行結(jié)果后進行現(xiàn)場清理,可以定義tearDown()方法:
import unittest
class B(unittest.TestCase):
def setUp(self):
self._value = 1
def test_b(self):
self.assertEqual(self._value, 1)
def tearDown(self):
del self._value
setUp()和tearDown()方法的執(zhí)行過程是:針對每一個測試項目,先執(zhí)行setUp()方法,如果成功,那么繼續(xù)執(zhí)行測試函數(shù),最后不管測試函數(shù)是否執(zhí)行成功,都執(zhí)行tearDown()方法;如果setUp()方法失敗,則認(rèn)為這個測試項目失敗,不會執(zhí)行測試函數(shù)也不執(zhí)行tearDown()方法。
工作中很多測試項目依賴相同的測試夾具(setUp和tearDown),unittest支持像這樣定義測試用例:
import unittest
class TestCase1(unittest.TestCase):
def setUp(self):
self._value = 12
def test_default(self):
self.assertEqual(self._value, 12, 'default value error')
def test_change(self):
self._value = 13
self.assertEqual(self._value, 13, 'change value fail')
如果要執(zhí)行指定的測試用例的話,可以使用TestSuite這個類,包含使用方法名作為參數(shù)聲明一個測試用例實例,比如:
import unittest
class TestCase1(unittest.TestCase):
def setUp(self):
self._value = 12
def test_default(self):
self.assertEqual(self._value, 12, 'default value error')
def test_change(self):
self._value = 13
self.assertEqual(self._value, 13, 'change value fail')
test_suite = unittest.TestSuite()
test_suite.addTest(TestCase1('test_default'))
test_runner = unittest.TextTestRunner()
test_runner.run(test_suite)
測試套也可以是測試套的集合,比如:
import unittest
class TestCase1(unittest.TestCase):
def setUp(self):
self._value = 12
def test_default(self):
self.assertEqual(self._value, 12, 'default value error')
def test_change(self):
self._value = 13
self.assertEqual(self._value, 13, 'change value fail')
test_suite1 = unittest.TestSuite()
test_suite1.addTest(TestCase1('test_default'))
test_suite2 = unittest.TestSuite()
test_suite2.addTest(TestCase1('test_change'))
test_suite = unittest.TestSuite([test_suite1, test_suite2])
test_runner = unittest.TextTestRunner()
test_runner.run(test_suite)
如果想執(zhí)行測試類中的部分測試用例,可以采用如下方式:
def suite():
tests = ['test_default', 'test_change']
return unittest.TestSuite(map(TestCase1, tests))
test_runner = unittest.TextTestRunner()
test_runner.run(suite())
因為將一個測試用例類下面的所有測試步驟都執(zhí)行一遍的情況非常普遍,unittest提
供了TestLoader類,它的loadTestsFromTestCase()方法會在一個TestCase類中尋找所有以test開頭的函數(shù)定義,并將他們添加到測試套中,這些函數(shù)會按照其名字的字符串排序順序執(zhí)行,比如:
import unittest
class TestCase1(unittest.TestCase):
def setUp(self):
self._value = 12
def test_default(self):
self.assertEqual(self._value, 12, 'default value error')
def test_change(self):
self._value = 13
self.assertEqual(self._value, 13, 'change value fail')
test_suite = unittest.TestLoader().loadTestsFromTestCase(TestCase1)
unittest.TextTestRunner(verbosity=2).run(test_suite)
忽略測試用例及假設(shè)用例失敗
有些情況下需要忽略執(zhí)行某些測試用例或者測試類,這個時候可以使用unittest.skip裝飾器及其變種。需要特別注意的是,可以通過skip某個測試類的setUp()方法而跳過整個測試類的執(zhí)行,比如:
import unittest, sys
version = (0, 1)
class HowToSkip(unittest.TestCase):
@unittest.skip('demonstrating skipping')
def test_nothing(self):
self.fail('will never be ran')
@unittest.skipIf(version < (1, 3),
'not supported version')
def test_format(self):
print 'your version is >= (1, 3)'
@unittest.skipUnless(sys.platform.startswith('win'),
'requires windows')
def test_winndows_support(self):
print 'support windows'
@unittest.skip('class can also be skipped')
class Skipped(unittest.TestCase):
def test_skip(self):
pass
class SkippedBySetUp(unittest.TestCase):
@unittest.skip('Skipped by setUp method')
def setUp(self):
pass
def test_dummy1(self):
print 'dummy1'
def test_dummy2(self):
print 'dummy2'
測試結(jié)果如下:
$ python -m unittest -v unit
test_format (unit4.HowToSkip) ... skipped 'not supported version'
test_nothing (unit4.HowToSkip) ... skipped 'demonstrating skipping'
test_winndows_support (unit4.HowToSkip) ... skipped 'requires windows'
test_skip (unit4.Skipped) ... skipped 'class can also be skipped'
test_dummy1 (unit4.SkippedBySetUp) ... skipped 'Skipped by setUp method'
test_dummy2 (unit4.SkippedBySetUp) ... skipped 'Skipped by setUp method'
----------------------------------------------------------------------
Ran 6 tests in 0.001s
OK (skipped=6)
特別地,被忽略的測試用例將不會執(zhí)行他們的setUp()、tearDown()方法,被忽略的測試類將不會執(zhí)行他們的setUpClass()、tearDownClass()方法(關(guān)于setUpClass()和tearDownClass()的詳細介紹,在下一篇博客中)。
有的時候,明知道某些測試用例會失敗,這時可以使用unittest.expectedFailure裝飾器,被期望失敗的測試用例不會加到測試結(jié)果的failure統(tǒng)計中,而是加到expected failure統(tǒng)計中,比如:
import unittest
class ExpectedFailure(unittest.TestCase):
@unittest.expectedFailure
def test_fail(self):
self.assertEqual(1, 2, 'not equal')
測試結(jié)果如下:
$ python -m unittest -v unit
test_fail (unit.ExpectedFailure) ... expected failure
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK (expected failures=1)
如果被expectedFailure的測試用例成功了,會被加到unexpected success的計數(shù)中。
綜上所述,unittest執(zhí)行測試用例結(jié)束后,有6種結(jié)束狀態(tài):ok、failure、error、expected failure、skipped、unexpected success。實際工作中發(fā)送自動化測試報告時,需要注意分別這些狀態(tài)的含義。
用Python搭建自動化測試框架,我們需要組織用例以及測試執(zhí)行,這里博主推薦Python的標(biāo)準(zhǔn)庫——unittest。
unittest是xUnit系列框架中的一員,如果你了解xUnit的其他成員,那你用unittest來應(yīng)該是很輕松的,它們的工作方式都差不多。
unittest核心工作原理
unittest中最核心的四個概念是:test case, test suite, test runner, test fixture。
下面我們分別來解釋這四個概念的意思,先來看一張unittest的靜態(tài)類圖(下面的類圖以及解釋均來源于網(wǎng)絡(luò),原文鏈接):
- 一個TestCase的實例就是一個測試用例。什么是測試用例呢?就是一個完整的測試流程,包括測試前準(zhǔn)備環(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é)果會保存到TextTestResult實例中,包括運行了多少測試用例,成功了多少,失敗了多少等信息。
- 而對一個測試用例環(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í)行時,默認(rèn)將執(zhí)行結(jié)果輸出到控制臺,我們可以設(shè)置其輸出到文件,在文件中查看結(jié)果(你可能聽說過HTMLTestRunner,是的,通過它可以將結(jié)果輸出到HTML中,生成漂亮的報告,它跟TextTestRunner是一樣的,從名字就能看出來,這個我們后面再說)。
unittest實例
下面我們通過一些實例來更好地認(rèn)識一下unittest。
我們先來準(zhǔn)備一些待測方法:
mathfunc.py
def add(a, b):
return a+b
def minus(a, b):
return a-b
def multi(a, b):
return a*b
def divide(a, b):
return a/b
簡單示例
接下來我們?yōu)檫@些方法寫一個測試:
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é)果的標(biāo)識,成功是
.,失敗是F,出錯是E,跳過是S。從上面也可以看出,測試的執(zhí)行跟方法的順序沒有關(guān)系,test_divide寫在了第4個,但是卻是第2個執(zhí)行的。每個測試方法均以
test開頭,否則是不被unittest識別的。在unittest.main()中加
verbosity參數(shù)可以控制輸出的錯誤報告的詳細程度,默認(rèn)是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。
將結(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格式的測試報告了。
test fixture之setUp() tearDown()
上面整個測試基本跑了下來,但可能會遇到點特殊的情況:如果我的測試需要在每次執(zhí)行之前準(zhǔn)備環(huán)境,或者在每次執(zhí)行完之后需要進行一些清理怎么辦?比如執(zhí)行前需要連接數(shù)據(jù)庫,執(zhí)行完成之后需要還原數(shù)據(jù)、斷開連接??偛荒苊總€測試方法中都添加準(zhǔn)備環(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用來為測試準(zhǔn)備環(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í)行之前準(zhǔn)備一次環(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
do something after test.Clean up.
...
do something before test.Prepare environment.
multi
do something after test.Clean up.
This tearDownClass() method only called once too.
可以看到setUpClass以及tearDownClass均只執(zhí)行了一次。
跳過某個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方法。
進階——用HTMLTestRunner輸出漂亮的HTML報告
我們能夠輸出txt格式的文本執(zhí)行報告了,但是文本報告太過簡陋,是不是想要更加高大上的HTML報告?但unittest自己可沒有帶HTML報告,我們只能求助于外部的庫了。
HTMLTestRunner是一個第三方的unittest HTML報告庫,首先我們下載HTMLTestRunner.py,并放到當(dāng)前目錄下,或者你的’C:\Python27\Lib’下,就可以導(dǎo)入運行了。
下載地址:
修改我們的 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ù)雜更漂亮的報告。
總結(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報告。