Python必會的單元測試框架 —— unittest


http://blog.csdn.net/huilan_same/article/details/52944782

unittest是xUnit系列框架中的一員,如果你了解xUnit的其他成員,那你用unittest來應該是很輕松的,它們的工作方式都差不多。

unittest核心工作原理

unittest中最核心的四個概念是:test case, test suite, test runner, test fixture。

下面我們分別來解釋這四個概念的意思,先來看一張unittest的靜態類圖(下面的類圖以及解釋均來源於網絡,原文鏈接):

unittest類圖

  • 一個TestCase的實例就是一個測試用例。什么是測試用例呢?就是一個完整的測試流程,包括測試前准備環境的搭建(setUp),執行測試代碼(run),以及測試后環境的還原(tearDown)。元測試(unit test)的本質也就在這里,一個測試用例是一個完整的測試單元,通過運行這個測試單元,可以對某一個問題進行驗證。

  • 而多個測試用例集合在一起,就是TestSuite,而且TestSuite也可以嵌套TestSuite。

  • TestLoader是用來加載TestCase到TestSuite中的,其中有幾個loadTestsFrom__()方法,就是從各個地方尋找TestCase,創建它們的實例,然后add到TestSuite中,再返回一個TestSuite實例。

  • TextTestRunner是來執行測試用例的,其中的run(test)會執行TestSuite/TestCase中的run(result)方法。 
    測試的結果會保存到TextTestResult實例中,包括運行了多少測試用例,成功了多少,失敗了多少等信息。

  • 而對一個測試用例環境的搭建和銷毀,是一個fixture。

一個class繼承了unittest.TestCase,便是一個測試用例,但如果其中有多個以 test 開頭的方法,那么每有一個這樣的方法,在load的時候便會生成一個TestCase實例,如:一個class中有四個test_xxx方法,最后在load到suite中時也有四個測試用例。

到這里整個流程就清楚了:

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

unittest實例

下面我們通過一些實例來更好地認識一下unittest。

我們先來准備一些待測方法:

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

 

簡單示例

接下來我們為這些方法寫一個測試:

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。

將結果輸出到文件中

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

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

test fixture之setUp() tearDown()

上面整個測試基本跑了下來,但可能會遇到點特殊的情況:如果我的測試需要在每次執行之前准備環境,或者在每次執行完之后需要進行一些清理怎么辦?比如執行前需要連接數據庫,執行完成之后需要還原數據、斷開連接。總不能每個測試方法中都添加准備環境、清理環境的代碼吧。

這就要涉及到我們之前說過的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 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均只執行了一次。

跳過某個case

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

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

  1. 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格式的文本執行報告了,但是文本報告太過簡陋,是不是想要更加高大上的HTML報告?但unittest自己可沒有帶HTML報告,我們只能求助於外部的庫了。

HTMLTestRunner是一個第三方的unittest HTML報告庫,首先我們下載HTMLTestRunner.py,並放到當前目錄下,或者你的’C:\Python27\Lib’下,就可以導入運行了。

下載地址:

官方原版:http://tungwaiyip.info/software/HTMLTestRunner.html

灰藍修改版:HTMLTestRunner.py(已調整格式,中文顯示)

修改我們的 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 report

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

總結一下:

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

我們這里沒有討論命令行的使用以及模塊級別的fixture,感興趣的同學可以自行搜索資料學習。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM