Python的單元測(cè)試工具——unittest小結(jié)

簡(jiǎn)介

unittest是Python的內(nèi)建模塊擦耀,是Python單元測(cè)試的事實(shí)標(biāo)準(zhǔn)铆惑,也叫PyUnit畔咧。使用unittest之前违诗,先了解如下幾個(gè)概念:

  • test case:測(cè)試用例某抓,可以通過(guò)創(chuàng)建unitest.TestCase類的子類創(chuàng)建一個(gè)測(cè)試用例纸兔。
  • test fixture:包含執(zhí)行測(cè)試用例前的測(cè)試準(zhǔn)備工作、測(cè)試用例執(zhí)行后的清理工作(分別對(duì)應(yīng)TestCase中的setUp()tearDown()方法)否副,測(cè)試準(zhǔn)備和測(cè)試清理的目的是保證每個(gè)測(cè)試用例執(zhí)行前后的系統(tǒng)狀態(tài)一致汉矿。
  • test suite:測(cè)試套,是測(cè)試用例备禀、測(cè)試套或者兩者的集合洲拇,用來(lái)將有關(guān)聯(lián)的測(cè)試項(xiàng)打包奈揍。
  • test runner:負(fù)責(zé)執(zhí)行測(cè)試并將結(jié)果展示給用戶,可以展示圖形或文字形式(unittest.TextTestRunner)的結(jié)果赋续,或者返回一個(gè)錯(cuò)誤碼標(biāo)識(shí)測(cè)試用例的執(zhí)行結(jié)果男翰。test runner提供了一個(gè)方法run(),接受一個(gè)unittest.TestSuiteunittest.TestCase實(shí)例作為參數(shù)纽乱,執(zhí)行對(duì)應(yīng)測(cè)試項(xiàng)目后返回測(cè)試結(jié)果unittest.TestResult對(duì)象蛾绎。

基本使用方法

定義測(cè)試用例的方法如下:

#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

每一個(gè)測(cè)試項(xiàng)目的函數(shù)定義以test開頭命名,這樣test runner就知道哪些函數(shù)是應(yīng)該被執(zhí)行的迫淹。上面的例子展示了驗(yàn)證測(cè)試結(jié)果常用的三種方法:

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

之所以不使用Python內(nèi)建的assert拋出異常肺稀,是因?yàn)閠est runner需要根據(jù)這些封裝后的方法拋出的異常做測(cè)試結(jié)果統(tǒng)計(jì)。

unittest.main()方法會(huì)在當(dāng)前模塊尋找所有unittest.TestCase的子類应民,并執(zhí)行它們中的所有測(cè)試項(xiàng)目。使用-v參數(shù)可以看到更詳細(xì)的測(cè)試執(zhí)行過(guò)程:

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

測(cè)試結(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

從命令行運(yùn)行unittest

$ python -m unittest unit          #直接運(yùn)行模塊unit中的測(cè)試用例
$ python -m unittest unit.TestStringMethods          #運(yùn)行模塊中的某個(gè)類
$ python -m unittest unit.TestStringMethods.test_upper          #運(yùn)行某個(gè)單獨(dú)的測(cè)試方法

混合運(yùn)行測(cè)試模塊归园、類以及測(cè)試方法也是可以的捻浦。

如果要查看unittest模塊命令行的更多參數(shù)信息朱灿,使用-h參數(shù):

$ python -m unittest -h
  • -b參數(shù):只在測(cè)試用例fail或者error時(shí)顯示它的stdout和stderr盗扒,否則不會(huì)顯示缕碎。

  • -f參數(shù):如果有一個(gè)測(cè)試用例fail或者出現(xiàn)error,立即停止測(cè)試栅贴。

  • -c參數(shù):捕捉Control-C信號(hào)檐薯,并顯示測(cè)試結(jié)果赚楚。

自動(dòng)發(fā)現(xiàn)測(cè)試用例

unittest能夠自動(dòng)發(fā)現(xiàn)測(cè)試用例。為了讓測(cè)試用例能夠被自動(dòng)發(fā)現(xiàn),測(cè)試文件需要是在項(xiàng)目目錄中可以import的module或者package,比如如下目錄結(jié)構(gòu):

unittest
├── test_a
│   ├── __init__.py
│   └── test_a.py
└── test_b.py

在unittest目錄中運(yùn)行如下命令泄私,即可運(yùn)行test_a這個(gè)package和test_b這個(gè)module中的測(cè)試項(xiàng)目:

$ python -m unittest discover
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

unittest discovery默認(rèn)會(huì)搜索名字命名符合test*的module或package咧纠,可以添加更多的參數(shù):

  • -v:詳細(xì)輸出亲轨。
  • -s:開始自動(dòng)搜索的目錄讯嫂,默認(rèn)是.巫湘;這個(gè)參數(shù)也可以指向一個(gè)package名,而不是目錄,例如unittest.test_a魂迄。
  • -p:文件匹配的模式,默認(rèn)是test*.py硬萍。
  • -t:項(xiàng)目頂級(jí)目錄窝撵,默認(rèn)與開始自動(dòng)搜索的目錄相同。

比如:

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

OK

測(cè)試用例發(fā)現(xiàn)通過(guò)import模塊或package執(zhí)行測(cè)試襟铭,例如foo/bar/baz.py會(huì)被import為foo.bar.baz

測(cè)試代碼的組織

測(cè)試用例一定要是自包含的短曾,即測(cè)試用例既可以獨(dú)立運(yùn)行寒砖,也可以和其他測(cè)試用例混合執(zhí)行,測(cè)試用例執(zhí)行前后不能影響系統(tǒng)狀態(tài)嫉拐。

建議將被測(cè)試代碼和測(cè)試代碼分離哩都,比如一個(gè)模塊module.py對(duì)應(yīng)的單元測(cè)試的文件是test_module.py,這樣方便維護(hù)婉徘。

最簡(jiǎn)單的測(cè)試用例定義漠嵌,是一個(gè)unittest.TestCase的子類只包含一個(gè)測(cè)試步驟,這個(gè)時(shí)候只需要定義一個(gè)runTest方法盖呼,比如:

# unit.py
import unittest

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

執(zhí)行測(cè)試后結(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*方法檢查失敗儒鹿,會(huì)拋出一個(gè)異常,unittest會(huì)將其算作失敿肝睢(failure)约炎。任何其他異常都被unittest算作錯(cuò)誤(error),比如:

#unit.py
import unittest

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

執(zhí)行測(cè)試結(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通常是實(shí)際結(jié)果與預(yù)期結(jié)果不符蟹瘾,error通常是因?yàn)闇y(cè)試代碼有bug導(dǎo)致圾浅。
如果很多測(cè)試項(xiàng)目的初始化準(zhǔn)備工作類似,可以為他們定義同一個(gè)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不會(huì)繼續(xù)執(zhí)行子類中的runTest方法。
如果想在測(cè)試項(xiàng)目執(zhí)行結(jié)果后進(jìn)行現(xiàn)場(chǎng)清理众雷,可以定義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í)行過(guò)程是:針對(duì)每一個(gè)測(cè)試項(xiàng)目灸拍,先執(zhí)行setUp()方法,如果成功砾省,那么繼續(xù)執(zhí)行測(cè)試函數(shù)株搔,最后不管測(cè)試函數(shù)是否執(zhí)行成功,都執(zhí)行tearDown()方法纯蛾;如果setUp()方法失敗纤房,則認(rèn)為這個(gè)測(cè)試項(xiàng)目失敗,不會(huì)執(zhí)行測(cè)試函數(shù)也不執(zhí)行tearDown()方法翻诉。

工作中很多測(cè)試項(xiàng)目依賴相同的測(cè)試夾具(setUptearDown)炮姨,unittest支持像這樣定義測(cè)試用例:

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í)行指定的測(cè)試用例的話捌刮,可以使用TestSuite這個(gè)類,包含使用方法名作為參數(shù)聲明一個(gè)測(cè)試用例實(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)

測(cè)試套也可以是測(cè)試套的集合绅作,比如:

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í)行測(cè)試類中的部分測(cè)試用例,可以采用如下方式:

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

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

因?yàn)閷⒁粋€(gè)測(cè)試用例類下面的所有測(cè)試步驟都執(zhí)行一遍的情況非常普遍蛾派,unittest提
供了TestLoader類俄认,它的loadTestsFromTestCase()方法會(huì)在一個(gè)TestCase類中尋找所有以test開頭的函數(shù)定義,并將他們添加到測(cè)試套中洪乍,這些函數(shù)會(huì)按照其名字的字符串排序順序執(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)

忽略測(cè)試用例及假設(shè)用例失敗

有些情況下需要忽略執(zhí)行某些測(cè)試用例或者測(cè)試類,這個(gè)時(shí)候可以使用unittest.skip裝飾器及其變種壳澳。需要特別注意的是岂贩,可以通過(guò)skip某個(gè)測(cè)試類的setUp()方法而跳過(guò)整個(gè)測(cè)試類的執(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'

測(cè)試結(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)

特別地巷波,被忽略的測(cè)試用例將不會(huì)執(zhí)行他們的setUp()萎津、tearDown()方法,被忽略的測(cè)試類將不會(huì)執(zhí)行他們的setUpClass()抹镊、tearDownClass()方法(關(guān)于setUpClass()和tearDownClass()的詳細(xì)介紹锉屈,在下一篇博客中)。

有的時(shí)候垮耳,明知道某些測(cè)試用例會(huì)失敗部念,這時(shí)可以使用unittest.expectedFailure裝飾器,被期望失敗的測(cè)試用例不會(huì)加到測(cè)試結(jié)果的failure統(tǒng)計(jì)中氨菇,而是加到expected failure統(tǒng)計(jì)中儡炼,比如:

import unittest

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

測(cè)試結(jié)果如下:

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

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

OK (expected failures=1)

如果被expectedFailure的測(cè)試用例成功了,會(huì)被加到unexpected success的計(jì)數(shù)中查蓉。
綜上所述乌询,unittest執(zhí)行測(cè)試用例結(jié)束后,有6種結(jié)束狀態(tài):ok豌研、failure妹田、error、expected failure鹃共、skipped鬼佣、unexpected success。實(shí)際工作中發(fā)送自動(dòng)化測(cè)試報(bào)告時(shí)霜浴,需要注意分別這些狀態(tài)的含義晶衷。

用Python搭建自動(dòng)化測(cè)試框架,我們需要組織用例以及測(cè)試執(zhí)行,這里博主推薦Python的標(biāo)準(zhǔn)庫(kù)——unittest晌纫。

unittest是xUnit系列框架中的一員税迷,如果你了解xUnit的其他成員,那你用unittest來(lái)應(yīng)該是很輕松的锹漱,它們的工作方式都差不多箭养。

unittest核心工作原理

unittest中最核心的四個(gè)概念是:test case, test suite, test runner, test fixture

下面我們分別來(lái)解釋這四個(gè)概念的意思哥牍,先來(lái)看一張unittest的靜態(tài)類圖(下面的類圖以及解釋均來(lái)源于網(wǎng)絡(luò)毕泌,原文鏈接):

unittest類圖
  • 一個(gè)TestCase的實(shí)例就是一個(gè)測(cè)試用例。什么是測(cè)試用例呢嗅辣?就是一個(gè)完整的測(cè)試流程撼泛,包括測(cè)試前準(zhǔn)備環(huán)境的搭建(setUp),執(zhí)行測(cè)試代碼(run)辩诞,以及測(cè)試后環(huán)境的還原(tearDown)。元測(cè)試(unit test)的本質(zhì)也就在這里纺涤,一個(gè)測(cè)試用例是一個(gè)完整的測(cè)試單元译暂,通過(guò)運(yùn)行這個(gè)測(cè)試單元,可以對(duì)某一個(gè)問(wèn)題進(jìn)行驗(yàn)證撩炊。
  • 而多個(gè)測(cè)試用例集合在一起外永,就是TestSuite,而且TestSuite也可以嵌套TestSuite拧咳。
  • TestLoader是用來(lái)加載TestCase到TestSuite中的伯顶,其中有幾個(gè)loadTestsFrom__()方法,就是從各個(gè)地方尋找TestCase骆膝,創(chuàng)建它們的實(shí)例祭衩,然后add到TestSuite中,再返回一個(gè)TestSuite實(shí)例阅签。
  • TextTestRunner是來(lái)執(zhí)行測(cè)試用例的掐暮,其中的run(test)會(huì)執(zhí)行TestSuite/TestCase中的run(result)方法。
    測(cè)試的結(jié)果會(huì)保存到TextTestResult實(shí)例中政钟,包括運(yùn)行了多少測(cè)試用例路克,成功了多少,失敗了多少等信息养交。
  • 而對(duì)一個(gè)測(cè)試用例環(huán)境的搭建和銷毀精算,是一個(gè)fixture。

一個(gè)class繼承了unittest.TestCase碎连,便是一個(gè)測(cè)試用例灰羽,但如果其中有多個(gè)以 test 開頭的方法,那么每有一個(gè)這樣的方法,在load的時(shí)候便會(huì)生成一個(gè)TestCase實(shí)例谦趣,如:一個(gè)class中有四個(gè)test_xxx方法疲吸,最后在load到suite中時(shí)也有四個(gè)測(cè)試用例。

到這里整個(gè)流程就清楚了:

寫好TestCase前鹅,然后由TestLoader加載TestCase到TestSuite摘悴,然后由TextTestRunner來(lái)運(yùn)行TestSuite,運(yùn)行的結(jié)果保存在TextTestResult中舰绘,我們通過(guò)命令行或者unittest.main()執(zhí)行時(shí)蹂喻,main會(huì)調(diào)用TextTestRunner中的run來(lái)執(zhí)行,或者我們可以直接通過(guò)TextTestRunner來(lái)執(zhí)行用例捂寿。這里加個(gè)說(shuō)明口四,在Runner執(zhí)行時(shí),默認(rèn)將執(zhí)行結(jié)果輸出到控制臺(tái)秦陋,我們可以設(shè)置其輸出到文件蔓彩,在文件中查看結(jié)果(你可能聽說(shuō)過(guò)HTMLTestRunner,是的驳概,通過(guò)它可以將結(jié)果輸出到HTML中赤嚼,生成漂亮的報(bào)告,它跟TextTestRunner是一樣的顺又,從名字就能看出來(lái)更卒,這個(gè)我們后面再說(shuō))。

unittest實(shí)例

下面我們通過(guò)一些實(shí)例來(lái)更好地認(rèn)識(shí)一下unittest稚照。

我們先來(lái)準(zhǔn)備一些待測(cè)方法:

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

簡(jiǎn)單示例

接下來(lái)我們?yōu)檫@些方法寫一個(gè)測(cè)試:

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)

能夠看到一共運(yùn)行了4個(gè)測(cè)試蹂空,失敗了1個(gè),并且給出了失敗原因果录,2.5 != 2 也就是說(shuō)我們的divide方法是有問(wèn)題的上枕。

這就是一個(gè)簡(jiǎn)單的測(cè)試,有幾點(diǎn)需要說(shuō)明的:

  1. 在第一行給出了每一個(gè)用例執(zhí)行的結(jié)果的標(biāo)識(shí)弱恒,成功是 .姿骏,失敗是 F,出錯(cuò)是 E斤彼,跳過(guò)是 S分瘦。從上面也可以看出,測(cè)試的執(zhí)行跟方法的順序沒(méi)有關(guān)系琉苇,test_divide寫在了第4個(gè)嘲玫,但是卻是第2個(gè)執(zhí)行的。

  2. 每個(gè)測(cè)試方法均以 test 開頭并扇,否則是不被unittest識(shí)別的去团。

  3. 在unittest.main()中加 verbosity 參數(shù)可以控制輸出的錯(cuò)誤報(bào)告的詳細(xì)程度,默認(rèn)是 1,如果設(shè)為 0土陪,則不輸出每一用例的執(zhí)行結(jié)果昼汗,即沒(méi)有上面的結(jié)果中的第1行;如果設(shè)為 2鬼雀,則輸出詳細(xì)的執(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)

可以看到,每一個(gè)用例的詳細(xì)執(zhí)行情況以及用例名源哩,用例描述均被輸出了出來(lái)(在測(cè)試方法下加代碼示例中的”“”Doc String”“”鞋吉,在用例執(zhí)行時(shí),會(huì)將該字符串作為此用例的描述励烦,加合適的注釋能夠使輸出的測(cè)試報(bào)告更加便于閱讀

組織TestSuite

上面的代碼示例了如何編寫一個(gè)簡(jiǎn)單的測(cè)試谓着,但有兩個(gè)問(wèn)題,我們?cè)趺纯刂朴美龍?zhí)行的順序呢坛掠?(這里的示例中的幾個(gè)測(cè)試方法并沒(méi)有一定關(guān)系赊锚,但之后你寫的用例可能會(huì)有先后關(guān)系,需要先執(zhí)行方法A屉栓,再執(zhí)行方法B)舷蒲,我們就要用到TestSuite了。我們添加到TestSuite中的case是會(huì)按照添加的順序執(zhí)行的系瓢。

問(wèn)題二是我們現(xiàn)在只有一個(gè)測(cè)試文件阿纤,我們直接執(zhí)行該文件即可句灌,但如果有多個(gè)測(cè)試文件夷陋,怎么進(jìn)行組織,總不能一個(gè)個(gè)文件執(zhí)行吧胰锌,答案也在TestSuite中骗绕。

下面來(lái)個(gè)例子:

在文件夾中我們?cè)傩陆ㄒ粋€(gè)文件,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í)行了三個(gè)case酬土,并且順序是按照我們添加進(jìn)suite的順序執(zhí)行的。

上面用了TestSuite的 addTests() 方法格带,并直接傳入了TestCase列表撤缴,我們還可以:

# 直接用addTest方法添加單個(gè)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的方法是無(wú)法對(duì)case進(jìn)行排序的虎眨,同時(shí),suite中也可以套suite。

將結(jié)果輸出到文件中

用例組織好了嗽桩,但結(jié)果只能輸出到控制臺(tái)岳守,這樣沒(méi)有辦法查看之前的執(zhí)行記錄,我們想將結(jié)果輸出到文件碌冶。很簡(jiǎn)單湿痢,看示例:

修改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í)行報(bào)告均輸出到了此文件中,這下我們便有了txt格式的測(cè)試報(bào)告了嫩挤。

test fixture之setUp() tearDown()

上面整個(gè)測(cè)試基本跑了下來(lái)害幅,但可能會(huì)遇到點(diǎn)特殊的情況:如果我的測(cè)試需要在每次執(zhí)行之前準(zhǔn)備環(huán)境,或者在每次執(zhí)行完之后需要進(jìn)行一些清理怎么辦岂昭?比如執(zhí)行前需要連接數(shù)據(jù)庫(kù)以现,執(zhí)行完成之后需要還原數(shù)據(jù)、斷開連接约啊∫囟簦總不能每個(gè)測(cè)試方法中都添加準(zhǔn)備環(huán)境、清理環(huán)境的代碼吧恰矩。

這就要涉及到我們之前說(shuō)過(guò)的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() 兩個(gè)方法(其實(shí)是重寫了TestCase的這兩個(gè)方法),這兩個(gè)方法在每個(gè)測(cè)試方法執(zhí)行前以及執(zhí)行后執(zhí)行一次外傅,setUp用來(lái)為測(cè)試準(zhǔn)備環(huán)境纪吮,tearDown用來(lái)清理環(huán)境,已備之后的測(cè)試萎胰。

我們?cè)賵?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í)行了一次冰肴。

跳過(guò)某個(gè)case

如果我們臨時(shí)想要跳過(guò)某個(gè)case不執(zhí)行怎么辦?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))

執(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個(gè)榔组,但divide()方法被skip了熙尉。

skip裝飾器一共有三個(gè) unittest.skip(reason)unittest.skipIf(condition, reason)搓扯、unittest.skipUnless(condition, reason)检痰,skip無(wú)條件跳過(guò),skipIf當(dāng)condition為True時(shí)跳過(guò)擅编,skipUnless當(dāng)condition為False時(shí)跳過(guò)攀细。

  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)

效果跟上面的裝飾器一樣箫踩,跳過(guò)了divide方法。

進(jìn)階——用HTMLTestRunner輸出漂亮的HTML報(bào)告

我們能夠輸出txt格式的文本執(zhí)行報(bào)告了谭贪,但是文本報(bào)告太過(guò)簡(jiǎn)陋境钟,是不是想要更加高大上的HTML報(bào)告?但unittest自己可沒(méi)有帶HTML報(bào)告俭识,我們只能求助于外部的庫(kù)了慨削。

HTMLTestRunner是一個(gè)第三方的unittest HTML報(bào)告庫(kù),首先我們下載HTMLTestRunner.py套媚,并放到當(dāng)前目錄下缚态,或者你的’C:\Python27\Lib’下,就可以導(dǎo)入運(yùn)行了堤瘤。

下載地址:

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

灰藍(lán)修改版:HTMLTestRunner.py(已調(dià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í)行時(shí)本辐,在控制臺(tái)我們能夠看到執(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測(cè)試報(bào)告,HTMLReport.html慎皱,如圖:

html report

這下漂亮的HTML報(bào)告也有了老虫。其實(shí)你能發(fā)現(xiàn),HTMLTestRunner的執(zhí)行方法跟TextTestRunner很相似茫多,你可以跟我上面的示例對(duì)比一下祈匙,就是把類圖中的runner換成了HTMLTestRunner,并將TestResult用HTML的形式展現(xiàn)出來(lái)天揖,如果你研究夠深夺欲,可以寫自己的runner,生成更復(fù)雜更漂亮的報(bào)告宝剖。

總結(jié)一下

  1. unittest是Python自帶的單元測(cè)試框架洁闰,我們可以用其來(lái)作為我們自動(dòng)化測(cè)試框架的用例組織執(zhí)行框架歉甚。
  2. unittest的流程:寫好TestCase万细,然后由TestLoader加載TestCase到TestSuite,然后由TextTestRunner來(lái)運(yùn)行TestSuite纸泄,運(yùn)行的結(jié)果保存在TextTestResult中赖钞,我們通過(guò)命令行或者unittest.main()執(zhí)行時(shí),main會(huì)調(diào)用TextTestRunner中的run來(lái)執(zhí)行聘裁,或者我們可以直接通過(guò)TextTestRunner來(lái)執(zhí)行用例雪营。
  3. 一個(gè)class繼承unittest.TestCase即是一個(gè)TestCase,其中以 test 開頭的方法在load時(shí)被加載為一個(gè)真正的TestCase衡便。
  4. verbosity參數(shù)可以控制執(zhí)行結(jié)果的輸出献起,0 是簡(jiǎn)單報(bào)告洋访、1 是一般報(bào)告、2 是詳細(xì)報(bào)告谴餐。
  5. 可以通過(guò)addTest和addTests向suite中添加case或suite姻政,可以用TestLoader的loadTestsFrom__()方法。
  6. setUp()岂嗓、tearDown()汁展、setUpClass()以及 tearDownClass()可以在用例執(zhí)行前布置環(huán)境,以及在用例執(zhí)行后清理環(huán)境
  7. 我們可以通過(guò)skip厌殉,skipIf食绿,skipUnless裝飾器跳過(guò)某個(gè)case,或者用TestCase.skipTest方法公罕。
  8. 參數(shù)中加stream器紧,可以將報(bào)告輸出到文件:可以用TextTestRunner輸出txt報(bào)告,以及可以用HTMLTestRunner輸出html報(bào)告楼眷。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末品洛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子摩桶,更是在濱河造成了極大的恐慌桥状,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件硝清,死亡現(xiàn)場(chǎng)離奇詭異辅斟,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)芦拿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門士飒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人蔗崎,你說(shuō)我怎么就攤上這事酵幕。” “怎么了缓苛?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵芳撒,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我未桥,道長(zhǎng)笔刹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任冬耿,我火速辦了婚禮舌菜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘亦镶。我一直安慰自己日月,他們只是感情好袱瓮,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著爱咬,像睡著了一般懂讯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上台颠,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天褐望,我揣著相機(jī)與錄音,去河邊找鬼串前。 笑死瘫里,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的荡碾。 我是一名探鬼主播谨读,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼坛吁!你這毒婦竟也來(lái)了劳殖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拨脉,失蹤者是張志新(化名)和其女友劉穎哆姻,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體玫膀,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡矛缨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了帖旨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片箕昭。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖解阅,靈堂內(nèi)的尸體忽然破棺而出落竹,到底是詐尸還是另有隱情,我是刑警寧澤货抄,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布述召,位于F島的核電站,受9級(jí)特大地震影響碉熄,放射性物質(zhì)發(fā)生泄漏桨武。R本人自食惡果不足惜肋拔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一锈津、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凉蜂,春花似錦琼梆、人聲如沸性誉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)错览。三九已至,卻和暖如春煌往,著一層夾襖步出監(jiān)牢的瞬間倾哺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工刽脖, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留羞海,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓曲管,卻偏偏與公主長(zhǎng)得像却邓,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子院水,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

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