Python的單元測試工具——unittest小結

簡介

unittest是Python的內建模塊,是Python單元測試的事實標準,也叫PyUnit。使用unittest之前,先了解如下幾個概念:

  • test case:測試用例,可以通過創建unitest.TestCase類的子類創建一個測試用例。
  • test fixture:包含執行測試用例前的測試準備工作、測試用例執行后的清理工作(分別對應TestCase中的setUp()tearDown()方法),測試準備和測試清理的目的是保證每個測試用例執行前后的系統狀態一致。
  • test suite:測試套,是測試用例、測試套或者兩者的集合,用來將有關聯的測試項打包。
  • test runner:負責執行測試并將結果展示給用戶,可以展示圖形或文字形式(unittest.TextTestRunner)的結果,或者返回一個錯誤碼標識測試用例的執行結果。test runner提供了一個方法run(),接受一個unittest.TestSuiteunittest.TestCase實例作為參數,執行對應測試項目后返回測試結果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()

執行腳本:

$ python unit.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

每一個測試項目的函數定義以test開頭命名,這樣test runner就知道哪些函數是應該被執行的。上面的例子展示了驗證測試結果常用的三種方法:

  • assertEqual(a, b):比較a == b
  • assertTrue(exp)assertFalse(exp):驗證bool(exp)為True或者False
  • assertRaises(Exception):驗證Exception被拋出。

之所以不使用Python內建的assert拋出異常,是因為test runner需要根據這些封裝后的方法拋出的異常做測試結果統計。

unittest.main()方法會在當前模塊尋找所有unittest.TestCase的子類,并執行它們中的所有測試項目。使用-v參數可以看到更詳細的測試執行過程:

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

測試結果如下:

$ 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模塊命令行的更多參數信息,使用-h參數:

$ python -m unittest -h
  • -b參數:只在測試用例fail或者error時顯示它的stdout和stderr,否則不會顯示。

  • -f參數:如果有一個測試用例fail或者出現error,立即停止測試。

  • -c參數:捕捉Control-C信號,并顯示測試結果。

自動發現測試用例

unittest能夠自動發現測試用例。為了讓測試用例能夠被自動發現,測試文件需要是在項目目錄中可以import的module或者package,比如如下目錄結構:

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默認會搜索名字命名符合test*的module或package,可以添加更多的參數:

  • -v:詳細輸出。
  • -s:開始自動搜索的目錄,默認是.;這個參數也可以指向一個package名,而不是目錄,例如unittest.test_a。
  • -p:文件匹配的模式,默認是test*.py
  • -t:項目頂級目錄,默認與開始自動搜索的目錄相同。

比如:

$ python -m unittest discover -s test_a
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

測試用例發現通過import模塊或package執行測試,例如foo/bar/baz.py會被import為foo.bar.baz

測試代碼的組織

測試用例一定要是自包含的,即測試用例既可以獨立運行,也可以和其他測試用例混合執行,測試用例執行前后不能影響系統狀態。

建議將被測試代碼和測試代碼分離,比如一個模塊module.py對應的單元測試的文件是test_module.py,這樣方便維護。

最簡單的測試用例定義,是一個unittest.TestCase的子類只包含一個測試步驟,這個時候只需要定義一個runTest方法,比如:

# unit.py
import unittest

class MyTestCase(unittest.TestCase):
    def runTest(self):
        self.assertEqual(1, 2, 'not equal')

執行測試后結果如下:

$ 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會將其算作失敗(failure)。任何其他異常都被unittest算作錯誤(error),比如:

#unit.py
import unittest

class MyTestCase(unittest.TestCase):
    def runTest(self):
        self.assertEqual(notexist, 2, 'not exist')

執行測試結果如下:

$ 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通常是實際結果與預期結果不符,error通常是因為測試代碼有bug導致。
如果很多測試項目的初始化準備工作類似,可以為他們定義同一個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')

如果基類BaseTestCasesetUp方法中拋出異常,unittest不會繼續執行子類中的runTest方法。
如果想在測試項目執行結果后進行現場清理,可以定義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()方法的執行過程是:針對每一個測試項目,先執行setUp()方法,如果成功,那么繼續執行測試函數,最后不管測試函數是否執行成功,都執行tearDown()方法;如果setUp()方法失敗,則認為這個測試項目失敗,不會執行測試函數也不執行tearDown()方法。

工作中很多測試項目依賴相同的測試夾具(setUptearDown),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')

如果要執行指定的測試用例的話,可以使用TestSuite這個類,包含使用方法名作為參數聲明一個測試用例實例,比如:

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)

如果想執行測試類中的部分測試用例,可以采用如下方式:

def suite():
    tests = ['test_default', 'test_change']
    return unittest.TestSuite(map(TestCase1, tests))

test_runner = unittest.TextTestRunner()
test_runner.run(suite())

因為將一個測試用例類下面的所有測試步驟都執行一遍的情況非常普遍,unittest提
供了TestLoader類,它的loadTestsFromTestCase()方法會在一個TestCase類中尋找所有以test開頭的函數定義,并將他們添加到測試套中,這些函數會按照其名字的字符串排序順序執行,比如:

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)

忽略測試用例及假設用例失敗

有些情況下需要忽略執行某些測試用例或者測試類,這個時候可以使用unittest.skip裝飾器及其變種。需要特別注意的是,可以通過skip某個測試類的setUp()方法而跳過整個測試類的執行,比如:

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'

測試結果如下:

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

特別地,被忽略的測試用例將不會執行他們的setUp()、tearDown()方法,被忽略的測試類將不會執行他們的setUpClass()、tearDownClass()方法(關于setUpClass()和tearDownClass()的詳細介紹,在下一篇博客中)。

有的時候,明知道某些測試用例會失敗,這時可以使用unittest.expectedFailure裝飾器,被期望失敗的測試用例不會加到測試結果的failure統計中,而是加到expected failure統計中,比如:

import unittest

class ExpectedFailure(unittest.TestCase):
    @unittest.expectedFailure
    def test_fail(self):
        self.assertEqual(1, 2, 'not equal')

測試結果如下:

$ python -m unittest -v unit
test_fail (unit.ExpectedFailure) ... expected failure

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK (expected failures=1)

如果被expectedFailure的測試用例成功了,會被加到unexpected success的計數中。
綜上所述,unittest執行測試用例結束后,有6種結束狀態:ok、failure、error、expected failure、skipped、unexpected success。實際工作中發送自動化測試報告時,需要注意分別這些狀態的含義。

用Python搭建自動化測試框架,我們需要組織用例以及測試執行,這里博主推薦Python的標準庫——unittest。

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報告。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容