unittest框架


About

unittest是Python內置的單元測試框架(模塊),不僅可以完成單元測試,也適用於web自動化測試中。

unittest提供了豐富的斷言方法,判斷測試用例是否通過,然后生成測試結果報告。

必要的准備與注意事項

首先,我們准備這樣一個目錄:

M:\tests\  # 我的是M盤的tests目錄,所有操作都在tests目錄內完成
    ├─discover   
    │  ├─son
    │  │  ├─test_dict.py 
    │  │  └─__init__.py
    │  ├─test_list.py
    │  ├─test_str.py
    │  └─__init__.py
    ├─loadTestsFromTestCaseDemo
    │  └─loadTestsFromTestCaseDemo.py
    ├─case_set.py
    ├─myMain.py   # 代碼演示文件,所有演示腳本文件
    ├─test_tuple.py
    └─__init__.py

如果你跟我的流程走, 請務必建立和理解這樣的一個目錄,目前這些文件都是空的,后續會一一建立,各目錄內的__init__.py也必須建立,雖然它是空的,但是它無比重要,因為它標明它所在目錄是Python的包。

case_set.py有4個函數,分別計算加減乘除,並且代碼不變:

"""
用例集
"""


def add(x, y):
    """ 兩數相加 """
    return x + y


def sub(x, y):
    """ 兩數相減 """
    return x - y


def mul(x, y):
    """ 兩數相乘 """
    return x * y


def div(x, y):
    """ 兩數相除 """
    return x / y


if __name__ == '__main__':
    print(div(10, 5))
    print(div(10, 0))

上述4個函數將成為我們的測試用例。

另外,示例演示環境是:

python3.6 + windows10 + pycharm2018.1

注意!注意!!注意!!!

如果你對pycharm的使用爛熟於心,那么在運行接下來的示例時,請不要右鍵運行或者點擊運行按鈕執行腳本,而是通過Terminal或者終端執行腳本,因為pycharm的集成環境會影響測試結果。

unittest簡單上手

runTest

import unittest  # 導入unittest框架
import case_set  # 導入用例集

class myUnitTest(unittest.TestCase):

    def setUp(self):
        """ 用例初始化 """
        print("用例初始化 setup")
    def runTest(self):
        """ 執行用例 """
        print(case_set.add(2, 3) == 5)
    def tearDown(self):
        """ 用例執行完,收尾 """
        print("用例執行完畢,收尾")
if __name__ == '__main__':
    demo = myUnitTest()
    demo.run()   # 固定的調用方法run

執行結果:

Ran 1 test in 0.002s

OK
用例初始化 setup
True
用例執行完畢,收尾

由結果可以看到,1個用例在多少時間內執行完畢,並且用例執行通過。

用例的執行流程是:

  • setUp先開第一槍,處理一些初始化操作。
  • 接着runTest執行用例,用例返回True。
  • 最后,tearDown打掃戰場!

在每個用例執行時,setUp和tearDown都會執行。

注意:

  • myUnitTest類名可以自定義,但是必須繼承unittest.TestCase
  • 示例中的setUp和tearDown方法名是固定的,但如果,我們測試用例時,沒有初始化和收尾的工作,setUp和tearDown方法可以省略不寫。

至於runTest方法名叫什么,取決於在實例化myUnitTest類時,是否傳參,我們來看unittest.TestCase類的__init__方法和run方法做了什么:

class TestCase(object):

    def __init__(self, methodName='runTest'):
        self._testMethodName = methodName
        self._outcome = None
        self._testMethodDoc = 'No test'  # 也請留意這個鬼東西 No test
    
    def run(self, result=None):
        # run方法反射了methodName
        testMethod = getattr(self, self._testMethodName)

可以看到,在實例化的時候,其實有個methodName默認參數,正好也叫runTest。而在實例化后,實例化對象調用run方法的時候,反射了那個methodName值,然后用例正常執行了。

所以,runTest方法名可以自定義:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

    def add_test(self):
        """ 執行用例 """
        print(case_set.add(2, 3) == 5)

if __name__ == '__main__':
    demo = myUnitTest(methodName='add_test')
    demo.run()

執行多個用例

那么,如果要執行多個用例怎么辦?

import unittest
import case_set

class myUnitTestAdd(unittest.TestCase):

    def runTest(self):
        """ 執行用例 """
        print(case_set.add(2, 3) == 5)

class myUnitTestSub(unittest.TestCase):

    def runTest(self):
        """ 執行用例 """
        print(case_set.sub(2, 3) == 5)  # 用例結果不符合預期

if __name__ == '__main__':
    demo1 = myUnitTestAdd()
    demo2 = myUnitTestSub()
    demo1.run()
    demo2.run()

上面的示例,也可以寫成:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

    def add_test(self):
        """ 執行用例 """
        print(case_set.add(2, 3) == 5)

    def sub_test(self):
        """ 執行用例"""
        print(case_set.sub(10, 5) == 2)

if __name__ == '__main__':
    demo1 = myUnitTest('add_test')
    demo2 = myUnitTest('sub_test')
    demo1.run()
    demo2.run()

如上方式,每個用例都要實例化一次,雖然可以執行多個用例,但是這么寫實在是太low了,反倒沒有之前測試除法用例來的簡單。

另外,用print打印也不符合真實的測試環境。

我們先來解決print的問題。

使用unittest提供的斷言

來看看unittest為我們提供了哪些斷言方法吧!

unittet.TestCase提供了一些斷言方法用來檢查並報告故障。

下表列出了最常用的方法:

Method Checks that description New in
assertEqual(a, b, msg) a == b 如果a不等於b,斷言失敗
assertNotEqual(a, b, msg) a != b 如果a等於b,斷言失敗
assertTrue(x, msg) bool(x) is True 如果表達式x不為True,斷言失敗
assertFalse(x, msg) bool(x) is False 如果表達式x不為False,斷言失敗
assertIs(a, b, msg) a is b 如果a is not 2,斷言失敗 3.1
assertIsNot(a, b, msg) a is not b 如果a is b,斷言失敗 3.1
assertIsNone(x, msg) x is not None 如果x不是None,斷言失敗 3.1
assertIn(a, b, msg) a in b 如果a not in b,斷言失敗 3.1
assertNotIn(a, b, msg) a not in b 如果a in b,斷言失敗 3.1
assertIsInstance(a, b, msg) isinstance(a, b) 如果a不是b類型,斷言失敗 3.2
assertNotIsInstance(a, b, msg) not isinstance(a, b) 如果a是b類型,斷言失敗 3.2

示例:

import unittest


class TestStringMethods(unittest.TestCase):

    def test_assertEqual(self):
        self.assertEqual(1, 2, msg='1 != 2')  # AssertionError: 1 != 2 : 1 != 2

    def test_assertTrue(self):
        self.assertTrue('')

    def test_assertFalse(self):
        self.assertFalse('')


if __name__ == '__main__':
    unittest.main()

所有的assert方法都接收一個msg參數,如果指定,該參數將用作失敗時的錯誤提示。

結果示例:

F.F
======================================================================
FAIL: test_assertEqual (__main__.TestStringMethods)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "myMain.py", line 251, in test_assertEqual
    self.assertEqual(1, 2, msg='1 != 2')  # AssertionError: 1 != 2 : 1 != 2
AssertionError: 1 != 2 : 1 != 2

======================================================================
FAIL: test_assertTrue (__main__.TestStringMethods)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "myMain.py", line 254, in test_assertTrue
    self.assertTrue('')
AssertionError: '' is not true

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=2)

結果中,F.F表示,如果用例通過返回.,失敗返回F,所以結果告訴我們執行了3個用例,成功1個,失敗兩個FAILED (failures=2)AssertionError是錯誤信息。

unittest.TestSuite

測試套件(test suite)是由許多測試用例組成的復合測試,也可以理解為承載多個用例集合的容器。
使用時需要創建一個TestSuite實例對象,然后使用該對象添加用例:

  • suite_obj.addTest(self, test),添加一個測試用例。
  • suite_obj.addTests(self, tests),添加多個測試用例。
  • 在實例化方法中添加測試用例。

當添加完所有用例后,該測試套件將被交給測試執行(運行)器,如TextTestRunner,該執行器會按照用例的添加順序執行各用例,並聚合結果。

TestSuite有效的解決了:

  • 因為是順序執行,當多個用例組成一個鏈式測試操作時,誰先誰后的問題就不存在了。
  • 有效地將多個用例組織到一起進行集中測試,解決了之前一個一個測試的問題。

suite_obj.addTest(self, test)

import unittest
import case_set

class myUnitTest(unittest.TestCase):

    def add_test(self):
        """ 執行用例 """
        self.assertEqual(case_set.add(2, 3), 5)

    def sub_test(self):
        """ 執行用例"""
        self.assertEqual(case_set.sub(10, 5), 5)

def create_suite():
    """ 創建用例集 """
    # 拿到兩個用例對象
    add = myUnitTest('add_test')
    sub = myUnitTest('sub_test')
    # 實例化suite對象
    suite_obj = unittest.TestSuite()
    # 添加用例
    suite_obj.addTest(add)
    suite_obj.addTest(sub)
    return suite_obj
    
if __name__ == '__main__':
    suite = create_suite()
    # 可以查看suite中的用例數量
    # print(suite.countTestCases())  # 2
    # 拿到執行器對象
    runner = unittest.TextTestRunner()
    # 你想用執行器執行誰?就把它傳進去
    runner.run(suite)

代碼注釋已經說得很明白了,只需要記住可以通過suite.countTestCases()方法獲取suite中用例的數量。

suite_obj.addTests(self, tests)

一個一個往suite中添加用例比較麻煩,所以,再來個簡單的:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

    def add_test(self):
        """ 執行用例 """
        self.assertEqual(case_set.add(2, 3), 5)

    def sub_test(self):
        """ 執行用例"""
        self.assertEqual(case_set.sub(10, 5), 5)


def create_suite():
    """ 創建用例集 """
    '''
    # 拿到兩個用例對象
    add = myUnitTest('add_test')
    sub = myUnitTest('sub_test')
    # 實例化suite對象
    suite_obj = unittest.TestSuite()
    # 添加用例
    suite_obj.addTests([add, sub])
    '''
    # 上面的代碼也可以這么寫
    map_obj = map(myUnitTest, ['add_test', 'sub_test'])
    suite_obj = unittest.TestSuite()
    suite_obj.addTests(map_obj)
    return suite_obj

if __name__ == '__main__':

    suite = create_suite()
    # 可以查看suite中的用例數量
    # print(suite.countTestCases())  # 2
    # 拿到執行器對象
    runner = unittest.TextTestRunner()
    # 你想用執行器執行誰?就把它傳進去
    runner.run(suite)

實例化時添加用例

import unittest
import case_set

class myUnitTest(unittest.TestCase):

    def add_test(self):
        """ 執行用例 """
        self.assertEqual(case_set.add(2, 3), 5)

    def sub_test(self):
        """ 執行用例"""
        self.assertEqual(case_set.sub(10, 5), 5)

def create_suite():
    """ 創建用例集 """
    map_obj = map(myUnitTest, ['add_test', 'sub_test'])
    suite_obj = unittest.TestSuite(tests=map_obj)
    return suite_obj

if __name__ == '__main__':
    suite = create_suite()
    runner = unittest.TextTestRunner()
    runner.run(suite)

怎么玩的呢?其實我們在實例化時做了添加用例的操作,以下示例演示了實例化的過程:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

    def add_test(self):
        """ 執行用例 """
        self.assertEqual(case_set.add(2, 3), 5)

    def sub_test(self):
        """ 執行用例"""
        self.assertEqual(case_set.sub(10, 5), 5)

class myUnitTestSuite(unittest.TestSuite):
    def __init__(self):
        # 當實例化suite對象時,傳遞用例
        map_obj = map(myUnitTest, ['add_test', 'sub_test'])
        # 調用父類的 __init__ 方法
        super().__init__(tests=map_obj)

if __name__ == '__main__':
    suite_obj = myUnitTestSuite()
    runner = unittest.TextTestRunner()
    runner.run(suite_obj)

雖然在一定程度上,我們優化了代碼,但是還不夠,因為,我們還需要手動的將用例添加到suite的中。接下來,我們來學習,如何自動添加。

unittest.makeSuite

import unittest
import case_set

class myUnitTest(unittest.TestCase):

    def add_test(self):
        self.assertEqual(case_set.add(2, 3), 5)

    def sub_test(self):
        self.assertEqual(case_set.sub(10, 5), 2)

    def test_mul(self):
        self.assertEqual(case_set.mul(10, 5), 50)

    def test_div(self):
        self.assertEqual(case_set.div(10, 5), 2)

def create_suite():
    """ 創建用例集 """
    suite_obj = unittest.makeSuite(testCaseClass=myUnitTest, prefix='test')
    return suite_obj

if __name__ == '__main__':
    suite_obj = create_suite()
    print(suite_obj.countTestCases())  # 2
    runner = unittest.TextTestRunner()
    runner.run(suite_obj)

想要自動添加,需要使用unittest.makeSuite類來完成,在實例化unittest.makeSuite(testCaseClass, prefix='test')時,需要告訴makeSuite添加用例的類名,上例是myUnitTest,然后makeSuite將myUnitTest類中所有以prefix參數指定開頭的用例,自動添加到suite中。

再次強調,prefix參數默認讀取以test開頭的用例,也可以自己指定:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

    def my_add_test(self):
        self.assertEqual(case_set.add(2, 3), 5)

    def my_sub_test(self):
        self.assertEqual(case_set.sub(10, 5), 2)  # AssertionError: 5 != 2

    def test_mul(self):
        self.assertEqual(case_set.mul(10, 5), 50)

    def test_div(self):
        self.assertEqual(case_set.div(10, 5), 2)

def create_suite():
    """ 創建用例集 """
    suite_obj = unittest.makeSuite(myUnitTest, prefix='my')
    return suite_obj

if __name__ == '__main__':
    suite_obj = create_suite()
    print(suite_obj.countTestCases())  # 2
    runner = unittest.TextTestRunner()
    runner.run(suite_obj)

如上例示例,讀取myUnitTest類中所有以my開頭的用例方法。但建議還是按照人家默認的test就好了。

除此之外,這都9102年了, 車車都是手自一體的,咱們除了能玩自動添加,也能手動的將指定的用例添加到suite中:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

    def my_add_test(self):
        self.assertEqual(case_set.add(2, 3), 5)

    def my_sub_test(self):
        self.assertEqual(case_set.sub(10, 5), 2)  # AssertionError: 5 != 2

    def test_mul(self):
        self.assertEqual(case_set.mul(10, 5), 50)

    def test_div(self):
        self.assertEqual(case_set.div(10, 5), 2)

def create_suite():
    """ 創建用例集 """
    suite_obj = unittest.makeSuite(myUnitTest, prefix='my')
    suite_obj.addTests(map(myUnitTest, ['test_mul', 'test_div']))
    return suite_obj

if __name__ == '__main__':
    suite_obj = create_suite()
    print(suite_obj.countTestCases())  # 4
    runner = unittest.TextTestRunner()
    runner.run(suite_obj)

上例,使用makeSuite自動添加所有以my開頭的用例,然后又使用addTests添加兩個用例。

unittest.TestLoader

到目前為止,我們所有的用例方法都封裝在一個用例類中,但是有的時候,我們會根據不同的功能編寫不同的測試用例文件,甚至是存放在不同的目錄內。

這個時候在用addTest添加就非常的麻煩了。
unittest提供了TestLoader類來解決這個問題。先看提供了哪些方法:

  • TestLoader.loadTestsFromTestCase,返回testCaseClass中包含的所有測試用例的suite。
  • TestLoader.loadTestsFromModule,返回包含在給定模塊中的所有測試用例的suite。
  • TestLoader.loadTestsFromName,返回指定字符串的所有測試用例的suite。
  • TestLoader.loadTestsFromNames,返回指定序列中的所有測試用例suite。
  • TestLoader.discover,從指定的目錄開始遞歸查找所有測試模塊。

執行腳本文件為myMain.py,目錄結構,參見開頭的目錄結構示例。
TestLoader.loadTestsFromTestCase

首先,loadTestsFromTestCaseDemo.py代碼如下:

import unittest

class LoadTestsFromTestCaseDemo(unittest.TestCase):

    def test_is_upper(self):
        self.assertTrue('FOO'.isupper())

    def test_is_lower(self):
        self.assertTrue('foo'.islower())

LoadTestsFromTestCaseDemo類中有兩個測試用例。

import unittest
from loadTestsFromTestCaseDemo.loadTestsFromTestCaseDemo import LoadTestsFromTestCaseDemo

class MyTestCase(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('FOO', 'foo'.upper())

if __name__ == '__main__':
    # 使用loadTestsFromTestCase獲取當前腳本和loadTestsFromTestCaseDemo腳本中的用例類
    test_case1 = unittest.TestLoader().loadTestsFromTestCase(MyTestCase)
    test_case2 = unittest.TestLoader().loadTestsFromTestCase(LoadTestsFromTestCaseDemo)
    # 創建suite並添加用例類
    suite = unittest.TestSuite()
    suite.addTests([test_case1, test_case2])
    unittest.TextTestRunner(verbosity=2).run(suite)

上例中,loadTestsFromTestCase需要傳入用例類的類名。無所謂這個用例類所處的目錄或者文件。

TestLoader.loadTestsFromModule

loadTestsFromTestCaseDemo.py代碼稍微有些變動:

import unittest

class LoadTestsFromTestCaseDemo1(unittest.TestCase):

    def test_is_upper(self):
        self.assertTrue('FOO'.isupper())

    def test_is_lower(self):
        self.assertTrue('foo'.islower())

class LoadTestsFromTestCaseDemo2(unittest.TestCase):

    def test_startswith(self):
        self.assertTrue('FOO'.startswith('F'))

    def test_endswith(self):
        self.assertTrue('foo'.endswith('o'))

再來看maMain.py

import unittest
from loadTestsFromTestCaseDemo import loadTestsFromTestCaseDemo

class MyTestCase(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('FOO', 'foo'.upper())

if __name__ == '__main__':
    # 使用 loadTestsFromTestCase 獲取當前腳本的用例類
    test_case1 = unittest.TestLoader().loadTestsFromTestCase(MyTestCase)
    # 使用 loadTestsFromModule 獲取 loadTestsFromTestCaseDemo 腳本中的用例類
    test_case2 = unittest.TestLoader().loadTestsFromModule(loadTestsFromTestCaseDemo)
    # 創建suite並添加用例類
    suite = unittest.TestSuite()
    suite.addTests([test_case1, test_case2])
    unittest.TextTestRunner(verbosity=2).run(suite)

上例中,loadTestsFromModule只要傳入用例類所在的腳本名即可。

TestLoader.loadTestsFromName && TestLoader.loadTestsFromNames
loadTestsFromTestCaseDemo.py代碼不變。
再來看maMain.py

import unittest
from loadTestsFromTestCaseDemo import loadTestsFromTestCaseDemo


class MyTestCase(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('FOO', 'foo'.upper())

if __name__ == '__main__':
    # 使用 loadTestsFromName 獲取當前腳本用例類的用例方法名稱
    test_case1 = unittest.TestLoader().loadTestsFromName(name='MyTestCase.test_upper', module=__import__(__name__))
    # 使用 loadTestsFromNames 獲取 loadTestsFromTestCaseDemo腳本中的LoadTestsFromTestCaseDemo1用例類的用例方法名
    test_case2 = unittest.TestLoader().loadTestsFromNames(
        names=['LoadTestsFromTestCaseDemo1.test_is_upper',
               'LoadTestsFromTestCaseDemo1.test_is_lower'
               ],
        module=loadTestsFromTestCaseDemo
    )
    # 創建suite並添加用例類
    suite = unittest.TestSuite()
    suite.addTests([test_case1, test_case2])
    unittest.TextTestRunner(verbosity=2).run(suite)

切記,無論是loadTestsFromName還是loadTestsFromNames,name參數都必須傳遞的是用例類下的方法名字,並且,方法名必須是全名。module參數就是腳本名字。

unittest.TestLoader().loadTestsFromNames(
	name="ClassName.MethodName",   # 類名點方法名
	module=ModuleName			   # 腳本名
)

TestLoader.discover
首先,創建一些測試用例,注意,一定要知道各文件所在的位置。
M:\tests\discover\test_list.py代碼如下:

import unittest

class TextCaseList(unittest.TestCase):

    def test_list_append(self):
        l = []
        l.append('a')
        self.assertEqual(l, ['a'])   # 判斷 l 是否等於 ['a']

    def test_list_remove(self):
        l = ['a']
        l.remove('a')
        self.assertEqual(l, [])

創建了兩個關於list的測試用例。
來看M:\tests\discover\test_str.py的代碼示例:

import unittest

class TextCaseStr(unittest.TestCase):

    def test_str_index(self):
        self.assertEqual('abc'.index('a'), 0)

    def test_str_find(self):
        self.assertEqual('abc'.find('a'), 0)

創建了兩個關於str的測試用例。
來看M:\tests\discover\son\test_dict.py的代碼示例:

import unittest

class TextCaseDict(unittest.TestCase):

    def test_dict_get(self):
        d = {'a': 1}
        self.assertEqual(d.get('a'), 1)

    def test_dict_pop(self):
        d = {'a': 1}
        self.assertEqual(d.pop('a'), 1)

創建了兩個關於dict的測試用例。
來看M:\tests\test_tuple.py的代碼示例:

import unittest

class TextCaseTuple(unittest.TestCase):

    def test_tuple_count(self):
        t = ('a', 'b')
        self.assertEqual(t.count('a'), 1)

    def test_tuple_index(self):
        t = ('a', 'b')
        self.assertEqual(t.index('a'), 0)

這樣,在不同的目錄中,新建了8個測試用例。
來研究一下discover怎么玩的。
discover部分無比重要,需要注意的地方有很多。要打起精神哦!
首先,來看discover的語法:

discover = unittest.TestLoader().discover(
	start_dir=base_dir,   # 該參必傳
	pattern='test*.py',   # 保持默認即可。
	top_level_dir=None
	)
unittest.TextTestRunner(verbosity=2).run(discover)

通過TestLoader()實例化對象,然后通過實例化對象調用discover方法,discover根據給定目錄,遞歸找到子目錄下的所有符合規則的測試模塊,然后交給TestSuit生成用例集suite。完事交給TextTestRunner執行用例。
該discover方法接收三個參數:

  • start_dir:要測試的模塊名或者測試用例的目錄。
  • pattern="test*.py":表示用例文件名的匹配原則,默認匹配以test開頭的文件名,星號表示后續的多個字符。
  • top_level_dir=None:測試模塊的頂層目錄,如果沒有頂層目錄,默認為None。

注意!!!意!!

  • discover對給定的目錄是有要求的,它只識別Python的包,也就是目錄內有__init__.py文件的才算是Python的包,只要是要讀取的目錄,都必須是包
  • 關於start_dir和top_level_dir的幾種情況:
    • start_dir目錄可以單獨指定,這個時候,讓top_level_dir保持默認(None)即可。
    • start_dir == top_level_dir, start_dir目錄與top_level_dir目錄一致,discover尋找start_dir指定目錄內的符合規則的模塊。
    • start_dir < top_level_dir,start_dir目錄是top_level_dir目錄的子目錄。discover尋找start_dir指定目錄內的符合規則的模塊。
    • start_dir > top_level_dir,start_dir目錄如果大於top_level_dir目錄,等待你的是報錯AssertionError: Path must be within the project。說你指定的路徑(start_dir)必須位於項目內(top_level_dir)。

這里再補充一點。
我們知道,TestLoader類根據各種標准加載測試用例,並將它們返回給測試套件(suite)。但一般的,我們也可以不需要創建這個類實例(想要用某個類的方法,通常都是通過個該類的實例化對象調用)。unittest已經幫我們實例化好了TestLoader對象————defaultTestLoader,我們可以直接使用defaultTestLoader.discover。

discover = unittest.defaultTestLoader.discover(
	start_dir=base_dir, 
	pattern='test*.py', 
	top_level_dir=base_dir
	)
unittest.TextTestRunner(verbosity=2).run(discover)

最后,仔細品味示例吧:

import os
import unittest

class MyTestCase(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('FOO', 'foo'.upper())

if __name__ == '__main__':
    base_dir = os.path.dirname(os.path.abspath(__name__))  # M:\tests
    discover_dir = os.path.join(base_dir, 'discover')  # M:\tests\discover
    son_dir = os.path.join(discover_dir, 'son')  # M:\tests\discover\son
    print(base_dir, discover_dir, son_dir)
    '''
    # start_dir 和top_level_dir 的目錄一致,獲取該 start_dir 目錄及子目錄內的所有以 test 開頭的 py 文件中的測試用例類
    discover = unittest.defaultTestLoader.discover(start_dir=base_dir, pattern='test*.py', top_level_dir=base_dir)
    unittest.TextTestRunner(verbosity=2).run(discover)  # 8個用例被執行
    '''
    # start_dir 是 top_level_dir 的子目錄,獲取該 start_dir 目錄及子目錄內的所有以 test 開頭的 py 文件中的測試用例類
    discover = unittest.defaultTestLoader.discover(start_dir=discover_dir, pattern='test*.py', top_level_dir=base_dir)
    unittest.TextTestRunner(verbosity=2).run(discover)  # 6個用例被執行
    
    # discover = unittest.TestLoader().discover(start_dir=base_dir)
    # unittest.TextTestRunner(verbosity=2).run(discover)

在參考示例時,心里默念注意事項。

一探unittest.main

現在,makeSuite雖然很好用,但是依然不夠,我們需要更加便捷和省事,一般情況下,我們更加傾向專注於編寫測試用例,而后直接使用unittest執行即可,希望makeSuite這一步都能由unittest來完成,而不是我們自己來。
是的,懶惰既是美德!Python或者unittest做到了:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

    def test_add(self):
        """ 測試加法用例 """
        print(self._testMethodName, self._testMethodDoc)   # test_add 測試加法用例
        self.assertEqual(case_set.add(2, 3), 5)

    def test_sub(self):
        self.assertEqual(case_set.sub(10, 5), 2) # AssertionError: 5 != 2

    def test_mul(self):
        self.assertEqual(case_set.mul(10, 5), 50)

    def test_div(self):
        self.assertEqual(case_set.div(10, 5), 2)

if __name__ == '__main__':
    unittest.main()

正如上例,我們只需要在用例類中將用例方法以test開頭,然后直接unittest.main()就可以直接測試了。
我想通過前面的鋪墊,這里也能大致的知道unittest.main()在內部做了什么了。我們將在最后來剖析它背后的故事。現在還有一些重要的事情等着我們。
另外,你也可以通過self._testMethodName來查看用例名稱;可以使用self._testMethodDoc來查看用例注釋(如果你寫了注釋的話)。

setUpClass && tearDownClass

在開始,我們學習了在測試某一個用例時,都會對應的執行三個方法:

  • setUp,開頭一槍的那家伙,它負責該用例之前可能需要的一些准備,比如連接數據庫。
  • runTest,執行用例邏輯,沒的說,干活的長工。
  • tearDown,負責打掃戰場,比如關閉數據庫連接。

示例:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

    def test_add(self):
        self.assertEqual(case_set.add(2, 3), 5)

    def test_sub(self):
        self.assertEqual(case_set.sub(10, 5), 5)

    def setUp(self):
        """ 如果myUnitTest中有我,我將在用例之前執行,無論我在myUnitTest的什么位置 """
        print('敵軍還有三十秒到達戰場, 碾碎他們....')

    def tearDown(self):
        """ 如果myUnitTest中有我,我將在用例之后執行,無論我在myUnitTest的什么位置 """
        print('ace .....')

if __name__ == '__main__':
    unittest.main()

結果:

敵軍還有三十秒到達戰場, 碾碎他們....
True
打完收工,阿sir出來洗地了.....
.敵軍還有三十秒到達戰場, 碾碎他們....
False
打完收工,阿sir出來洗地了.....
.
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

由結果可以看到,有兩個用例被執行並通過,並且,每一個用例執行前后都觸發了setUp和tearDown方法執行。
但是,同志們,如果這是由1000甚至更多的用例組成的用例集,並且每一個用例都去操作數據,那么每個用例都會做連接/關閉數據庫的操作。這就蛋疼了,就不能一次連接,所有用例都完事后,再關閉?這一下一下的......
是的,可以解決這個問題:

import unittest
import case_set

class myUnitTest(unittest.TestCase):

    def test_add(self):
        self.assertEqual(case_set.add(2, 3), 5)

    def test_sub(self):
        self.assertEqual(case_set.sub(10, 5), 5)

    def setUp(self):
        print('敵軍還有三十秒到達戰場, 碾碎他們....')

    def tearDown(self):
        print('打完收工,阿sir出來洗地了.....')

    @classmethod
    def setUpClass(cls):
        print('在用例集開始執行,我去建立數據庫連接......')

    @classmethod
    def tearDownClass(cls):
        print('全軍撤退, 我收工.......')

if __name__ == '__main__':
    unittest.main()

結果:

在用例集開始執行,我去建立數據庫連接......
敵軍還有三十秒到達戰場, 碾碎他們....
True
打完收工,阿sir出來洗地了.....
.敵軍還有三十秒到達戰場, 碾碎他們....
False
打完收工,阿sir出來洗地了.....
.全軍撤退, 我收工.......

----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK

由結果可以看到,setUpClasstearDownClass這兩個類方法完美的解決我們的問題,這讓我們在某些情況下可以更加靈活的組織邏輯。

verbosity參數

verbosity
上述的斷言結果雖然很清晰,但是還不夠!我們可以控制錯誤輸出的詳細程度。

import unittest

class TestStringMethods(unittest.TestCase):

    def test_assertFalse(self):
        self.assertFalse('')

if __name__ == '__main__':
    unittest.main(verbosity=1)

在執行unittest.main(verbosity=1)時,可以通過verbosity參數來控制錯誤信息的詳細程度。
verbosity=0

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

OK

verbosity=1

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

OK

verbosity=2

test_assertFalse (__main__.TestStringMethods) ... ok

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

OK

由結果可以總結,verbosity有3種的錯誤信息狀態提示報告:

  • 0,靜默模式,對於測試結果給予簡單提示。
  • 1,默認模式,與靜默模式類似,只是在每個成功的用例前面有個.每個失敗的用例前面有個F,跳過的用例有個S
  • 2,詳細模式,測試結果會顯示每個用例的所有相關的信息。

切記,只有0、1、2三種狀態。
默認的是1。
-v
除此之外,我們在終端執行時也可以輸出詳細報告:

M:\tests>python36 myMain.py -v
test_assertFalse (__main__.TestStringMethods) ... ok

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

OK

如上示例,使verbosity參數保持默認,我們通過在終端加-v來輸入詳細報告信息。

除了-v,還可以有:

M:\tests>python36 myMain.py -p    # 等效於verbosity=0

什么都不加,就是verbosity=1

跳過測試用例:skip

從Python3.1版本開始,unittest支持跳過單個測試方法甚至整個測試類。
也就是說,某些情況下,我們需要跳過指定的用例。
我們可以使用unittest提供的相關裝飾器來完成:

decorators description
@unittest.skip(reason) 無條件地跳過裝飾測試用例。 理由應該描述為什么跳過測試用例。
@unittest.skipIf(condition, reason) 如果條件為真,則跳過修飾的測試用例。
@unittest.skipUnless(condition, reason) 除非條件為真,否則跳過修飾的測試用例。
@unittest.expectedFailure 將測試標記為預期的失敗。如果測試失敗,將被視為成功。如果測試用例通過,則認為是失敗。
expection unittest.SkipTest(reason) 引發此異常以跳過測試測試用例。

示例:

import unittest

class TestCase01(unittest.TestCase):

    def test_assertTrue(self):
        self.assertTrue('')

    @unittest.skip('no test')  # 跳過該條用例
    def test_assertFalse(self):
        self.assertFalse('')

@unittest.skip('no test')  # 跳過這個用例類
class TestCase02(unittest.TestCase):

    def test_assertTrue(self):
        self.assertTrue('')

    def test_assertFalse(self):
        self.assertFalse('')

if __name__ == '__main__':
    unittest.main()

看結果:

M:\tests>python36 myMain.py
sFss
======================================================================
FAIL: test_assertTrue (__main__.TestCase01)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "demo0.py", line 27, in test_assertTrue
    self.assertTrue('')
AssertionError: '' is not true

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=1, skipped=3)

毋庸置疑,在結果中,總共4個用例,一個用例類被跳過,另一個用例類中跳過一個方法,那么就是執行4個用例,跳過3個

再探unittest.main

在解釋器的Lib\unittest框架內,主要目錄和文件,故事將會在這里展開。

**\Lib\unittest\
    ├─test\		# 目錄
    ├─case.py
    ├─loader.py
    ├─main.py
    ├─mock.py
    ├─result.py
    ├─runner.py
    ├─signals.py
    ├─suite.py
    ├─util.py
    ├─__init__.py
    └─__main__.py

現在,我們在腳本中執行這樣一段代碼:

import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())

if __name__ == '__main__':
    unittest.main()

當我們在終端執行:

M:\tests>python36 myMain.py -v
test_isupper (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

unittest源碼是這樣執行的.........

main.py文件中。
main = TestProgram

class TestProgram(object):
    # defaults for testing
    module=None
    verbosity = 1
    failfast = catchbreak = buffer = progName = warnings = None
    _discovery_parser = None

    def __init__(self, module='__main__', defaultTest=None, argv=None,
                    testRunner=None, testLoader=loader.defaultTestLoader,
                    exit=True, verbosity=1, failfast=None, catchbreak=None,
                    buffer=None, warnings=None, *, tb_locals=False):
        print(argv)  # ['myMain.py', '-v']
        self.parseArgs(argv)   # 檢查參數 
        self.runTests()   # 執行測試用例集
main = TestProgram

首先可以看到,main = TestProgram,所以,unittest.main()相當於unittest.TestProgram()。類加括號是實例化的過程,所以,我們將目光集中在__init__方法中,為實例化對象添加屬性我們先略過,主要來看在這里都是執行了哪些方法。
可以看到主要做了兩件事,self.parseArgs(argv)檢查終端是否有參數傳入,是有參數-v的。完事執行self.runTests()
先來研究檢查參數的self.parseArgs方法做了什么?

main.py: TestProgram.parseArgs

class TestProgram(object):
    def parseArgs(self, argv):
        self.createTests()

parseArgs經過一系列的操作,我們來到該方法的最后一行,self.createTests(),見名知意,這家伙是要創建用例集啊,看看具體怎么玩的。
main.py: TestProgram.createTests

class TestProgram(object):
    def createTests(self):
    	# self.testNames: None
    	# self.module: <module '__main__' from 'myMain.py'>
        if self.testNames is None:
            self.test = self.testLoader.loadTestsFromModule(self.module)
        else:
            self.test = self.testLoader.loadTestsFromNames(self.testNames, self.module)

首先判斷self.testNames是不是為None,這個參數是TestProgram.__init__(defaultTest=None)中的defaultTest參數,我們並沒有傳參,所以是None,那么就執行if條件。在if條件中執行了self.testLoader.loadTestsFromModule(self.module)方法,並傳遞了self.module參數,該參數其實就是我們運行的腳本文件名。
我們看看這個self.testLoader.loadTestsFromModule方法做了什么。
loader.py: TestLoader.loadTestsFromModule
loadTestsFromModule方法位於unittest框架下的loader.pyTestLoader類中。

class TestLoader(object):
    """ 根據各種標准生成測試用例集 """

    def loadTestsFromModule(self, module, *args, pattern=None, **kws):
        """返回給定模塊中用例類(可能有多個用例類)中的用例 suite """

        tests = []
        # dir(module)獲取 myMain.py中所有頂級屬性,包括類名、函數名、變量名
        # 循環判斷每一個屬性並判斷是否是case.TestCase的派生類
        for name in dir(module):
            obj = getattr(module, name)
            # 如果是case.TestCase的派生類,就添加到tests的列表中
            # 但在添加之前,要做類型檢查判斷
            if isinstance(obj, type) and issubclass(obj, case.TestCase):
                tests.append(self.loadTestsFromTestCase(obj))
        # module:myMain.py
        # module中沒有 load_tests,所以 load_tests為None
        load_tests = getattr(module, 'load_tests', None)
        tests = self.suiteClass(tests)
        # 因為load_tests為None,所以if語句不會執行,
        if load_tests is not None:
            try:
                return load_tests(self, tests, pattern)
            except Exception as e:
                error_case, error_message = _make_failed_load_tests(
                    module.__name__, e, self.suiteClass)
                self.errors.append(error_message)
                return error_case
        # tests: <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<__main__.TestStringMethods testMethod=test_isupper>, <__main__.TestStringMethods testMethod=test_upper>]>]>
        return tests  # 用例集 suite

    def loadTestsFromTestCase(self, testCaseClass):
        """返回testCaseClass中包含的所有測試用例的 suite"""
        # testCaseClass:是myMain.py中的用例類名 <class '__main__.TestStringMethods'>
        if issubclass(testCaseClass, suite.TestSuite):
            raise TypeError("Test cases should not be derived from "
                            "TestSuite. Maybe you meant to derive from "
                            "TestCase?")
        # 獲取testCaseClass中的所有以prefix指定的用例名
        testCaseNames = self.getTestCaseNames(testCaseClass)
        # print(testCaseClass, testCaseNames)  # <class '__main__.TestStringMethods'> ['test_isupper', 'test_upper']
        # 很明顯,咱們的腳本中沒有runTest
        if not testCaseNames and hasattr(testCaseClass, 'runTest'):
            testCaseNames = ['runTest']
        # 這就很明顯了 self.suiteClass(map(testCaseClass, testCaseNames)) 在生成用例集的suite
        loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))
        # loaded_suite:<unittest.suite.TestSuite tests=[<__main__.TestStringMethods testMethod=test_isupper>, <__main__.TestStringMethods testMethod=test_upper>]>
        # loaded_suite.countTestCases(): 2
        return loaded_suite  # 返回用例集 suite

    def getTestCaseNames(self, testCaseClass):
        """
            返回在testCaseClass中找到的方法名的排序序列
        """

        # self.testMethodPrefix:test
        def isTestMethod(attrname, testCaseClass=testCaseClass,
                         prefix=self.testMethodPrefix):
            return attrname.startswith(prefix) and \
                   callable(getattr(testCaseClass, attrname))

        testFnNames = list(filter(isTestMethod, dir(testCaseClass)))
        if self.sortTestMethodsUsing:
            testFnNames.sort(key=functools.cmp_to_key(self.sortTestMethodsUsing))
        return testFnNames  # ['test_isupper', 'test_upper']

loader.py中的TestLoader中一共做了三件事:

  • main.py: TestProgram.createTests方法觸發了loader.py: TestLoader.loadTestsFromModule方法執行,在這個方法中,首先循環判斷取出測試腳本中的所有的用例類。
  • 然后在循環判斷中,如果判斷測試腳本中的類是case.TestCase的派生類,就調用loader.py: TestLoader.loadTestsFromTestCase方法調用loader.py: TestLoader.getTestCaseNames並將用例類傳遞進去,該方法獲取到傳過來的用例類名,然后去這個用例類中去找所有prefix開頭的用例,然后以列表的形式返回給loader.py: TestLoader.loadTestsFromTestCase方法。
  • loader.py: TestLoader.loadTestsFromTestCase方法拿到用例列表后,生成用例集suite並返回調用者。

程序在loader.py執行完畢,回到main.py: TestProgram.createTests中。
main.py: TestProgram.createTests成功完成任務,生成了用例集 suite。程序再次回到了調用main.py: TestProgram.createTests的方法中——
main.py: TestProgram.parseArgs,然后main.py: TestProgram.parseArgs方法也執行完畢。程序繼續回到調用處——main.py: TestProgram.__init__方法中。
此時,創建用例集的suite完成。
程序由此繼續往下執行。
main.py: TestProgram.runTests
有了用例集就要執行了,往下看。

class TestProgram(object):
    def runTests(self):
        # 實例化時沒有傳參,所以 self.catchbreak: None
        if self.catchbreak:
            installHandler()
        # self.testRunner同樣沒有傳參,為None
        if self.testRunner is None:
            # runner.TextTestRunner是runner.py中的TextTestRunner對象
            self.testRunner = runner.TextTestRunner

        if isinstance(self.testRunner, type):
            try:
                try:
                    testRunner = self.testRunner(verbosity=self.verbosity,
                                                 failfast=self.failfast,
                                                 buffer=self.buffer,
                                                 warnings=self.warnings,
                                                 tb_locals=self.tb_locals)
                except TypeError:
                    # didn't accept the tb_locals argument
                    testRunner = self.testRunner(verbosity=self.verbosity,
                                                 failfast=self.failfast,
                                                 buffer=self.buffer,
                                                 warnings=self.warnings)
            except TypeError:
                # didn't accept the verbosity, buffer or failfast arguments
                testRunner = self.testRunner()
        else:
            # it is assumed to be a TestRunner instance
            testRunner = self.testRunner
            # 實例化runner.py中的TextTestRunner類得到testRunner對象
        # testRunner.run(self.test)依次執行每一個用例
        # 將結果收集到self.result中
        # self.test:<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<__main__.TestStringMethods testMethod=test_isupper>, <__main__.TestStringMethods testMethod=test_upper>]>]>

        self.result = testRunner.run(self.test)
        if self.exit:
            # self.result.wasSuccessful(): <bound method TestResult.wasSuccessful of <unittest.runner.TextTestResult run=2 errors
=0 failures=0>>

            sys.exit(not self.result.wasSuccessful())

執行用例沒啥好說的,調用了runner.py: TextTestRunner.run方法依次執行每個用例並收集結果。

runner.py: TextTestRunner.run

class TextTestResult(result.TestResult):
    """ 一個測試結果類,它可以將格式化的文本結果打印到流中 """
class TextTestRunner(object):
    """ 以文本形式顯示結果的測試運行器 """
    def __init__(self, stream=None, descriptions=True, verbosity=1,
                 failfast=False, buffer=False, resultclass=None, warnings=None,
                 *, tb_locals=False):
        """ 構造一個TextTestRunner. """
        # sys.stderr:  <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>
        # sys.stderr將結果輸出到屏幕
        if stream is None:
            stream = sys.stderr
        self.stream = _WritelnDecorator(stream)  # _WritelnDecorator:文件類的裝飾對象
        self.descriptions = descriptions
        self.verbosity = verbosity
        self.failfast = failfast
        self.buffer = buffer
        self.tb_locals = tb_locals
        self.warnings = warnings
        if resultclass is not None:
            # self.resultclass: TextTestResult
            self.resultclass = resultclass

    def _makeResult(self):
        # print(self.stream, self.descriptions, self.verbosity)  # <unittest.runner._WritelnDecorator object at 0x0373D690> True 2
        # 返回 TextTestResult 實例化對象
        return self.resultclass(self.stream, self.descriptions, self.verbosity)

    def run(self, test):
        "運行給定的測試用例或測試套件"
        # result: TextTestResult 實例化對象
        result = self._makeResult()
        registerResult(result)
        '''
        failfast是 TextTestRunner 的一個屬性,缺省為False
        作用: 如果failfast為True,一旦測試集中有測試案例failed或發生error立即終止當前整個測試執行,跳過剩下所有測試案例,也就是實現“短路測試”
        '''
        result.failfast = self.failfast  # self.failfast: False
        result.buffer = self.buffer
        result.tb_locals = self.tb_locals
        with warnings.catch_warnings():
            if self.warnings:
                # if self.warnings is set, use it to filter all the warnings
                warnings.simplefilter(self.warnings)
                # if the filter is 'default' or 'always', special-case the
                # warnings from the deprecated unittest methods to show them
                # no more than once per module, because they can be fairly
                # noisy.  The -Wd and -Wa flags can be used to bypass this
                # only when self.warnings is None.
                if self.warnings in ['default', 'always']:
                    warnings.filterwarnings('module',
                            category=DeprecationWarning,
                            message=r'Please use assert\w+ instead.')
            startTime = time.time()
            # result: TextTestResult
            # TextTestResult中並沒有 startTestRun,但是父類的 TestResult 中有
            startTestRun = getattr(result, 'startTestRun', None)
            if startTestRun is not None:
                # 執行TestResult的startTestRun
                startTestRun()
            try:
                # BaseTestSuite執行了 __call__ 方法,test加括號等於執行了 BaseTestSuite 的 run 方法
                test(result)
            finally:
                # 用例執行完畢,觸發 TestResult 的 stopTestRun 方法
                stopTestRun = getattr(result, 'stopTestRun', None)
                if stopTestRun is not None:
                    stopTestRun()
            stopTime = time.time()
        timeTaken = stopTime - startTime
        result.printErrors()
        if hasattr(result, 'separator2'):
            self.stream.writeln(result.separator2)
        run = result.testsRun
        self.stream.writeln("Ran %d test%s in %.3fs" %
                            (run, run != 1 and "s" or "", timeTaken))
        self.stream.writeln()

        expectedFails = unexpectedSuccesses = skipped = 0
        try:
            results = map(len, (result.expectedFailures,
                                result.unexpectedSuccesses,
                                result.skipped))
        except AttributeError:
            pass
        else:
            expectedFails, unexpectedSuccesses, skipped = results

        infos = []
        if not result.wasSuccessful():
            self.stream.write("FAILED")
            failed, errored = len(result.failures), len(result.errors)
            if failed:
                infos.append("failures=%d" % failed)
            if errored:
                infos.append("errors=%d" % errored)
        else:
            self.stream.write("OK")
        if skipped:
            infos.append("skipped=%d" % skipped)
        if expectedFails:
            infos.append("expected failures=%d" % expectedFails)
        if unexpectedSuccesses:
            infos.append("unexpected successes=%d" % unexpectedSuccesses)
        if infos:
            self.stream.writeln(" (%s)" % (", ".join(infos),))
        else:
            self.stream.write("\n")
        return result

總結:

  • 收集用例。
  • 根據用例生成測試集。
  • 運行測試集。

自定義刪除用例方法

我們之前學習unittest.makeSuite時,學過兩個添加用例的方法,但是我講過刪除用的方法了嗎?並沒有!現在,我們已經剖析了源碼,知道了添加用例是addTestaddTests干的。
suite.py: BaseTestSuite:

class BaseTestSuite(object):

    def addTest(self, test):
        # sanity checks
        if not callable(test):
            raise TypeError("{} is not callable".format(repr(test)))
        if isinstance(test, type) and issubclass(test,
                                                 (case.TestCase, TestSuite)):
            raise TypeError("TestCases and TestSuites must be instantiated "
                            "before passing them to addTest()")
        self._tests.append(test)

    def addTests(self, tests):
        if isinstance(tests, str):
            raise TypeError("tests must be an iterable of tests, not a string")
        for test in tests:
            self.addTest(test)

可以看到,addTest是一個一個添加,而addTests則是for循環調用addTest添加,本質上一樣的。
讓我們將目光聚集到addTest中,可以看到使用的是self._test.append(test)。現在,我們的刪除方法也有了——把添加方法復制一份,改幾個字即可:

class BaseTestSuite(object):

    def addTest(self, test):
        # sanity checks
        if not callable(test):
            raise TypeError("{} is not callable".format(repr(test)))
        if isinstance(test, type) and issubclass(test,
                                                 (case.TestCase, TestSuite)):
            raise TypeError("TestCases and TestSuites must be instantiated "
                            "before passing them to addTest()")
        self._tests.append(test)

    def removeTest(self, test):
        # sanity checks
        if not callable(test):
            raise TypeError("{} is not callable".format(repr(test)))
        if isinstance(test, type) and issubclass(test,
                                                 (case.TestCase, TestSuite)):
            raise TypeError("TestCases and TestSuites must be instantiated "
                            "before passing them to addTest()")
        self._tests.remove(test)

沒錯,你沒看錯,就是把addTest復制一份,方法名改為removeTest,完事把self._tests.append(test)改為self._tests.remove(test)就行了。

調用也類似:

import unittest


class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())

if __name__ == '__main__':
    case = TestStringMethods('test_upper')
    suite = unittest.TestSuite()
    suite.addTest(case)   # suite中有一個test_upper用例
    print(suite.countTestCases())  # 1
    suite.removeTest(case)  # 刪除掉它
    print(suite.countTestCases())  # 0

將執行結果輸出到文件

我們嘗試着講用例執行結果輸出到文件中。

import unittest
class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())

if __name__ == '__main__':
    f = open(r'M:\tests\t1.txt', 'w', encoding='utf-8')
    suite = unittest.makeSuite(TestStringMethods)
    unittest.TextTestRunner(stream=f).run(suite)

生成用例報告

如上小節中,雖然能將結果輸出到某個文件中,但更多的是根據模板生成報告,這里就來研究一下,如何生成模板報告。

參見:https://www.cnblogs.com/Neeo/articles/7942613.html

發送測試報告郵件

參見:https://www.cnblogs.com/Neeo/articles/11478853.html

unittest.mock

參見:https://www.cnblogs.com/Neeo/articles/11511103.html

小結:在unittest中,我們需要掌握的幾個類:

  • unittest.TestCase:所有測試用例的基類,給定一個測試用例方法的名字,就會返回一個測試用例實例。
  • unittest.TestSuite:組織測試用例的用例集,支持測試用例的添加和刪除。
  • unittest.TextTestRunner:測試用例的執行,其中Text是以文本的形式顯示測試結果,測試結果會保存到TextTestResult中。
  • unittest.TextTestResult:保存測試用例信息,包括運行了多少個測試用例,成功了多少,失敗了多少等信息。
  • unittest.TestLoader:加載TestCase到TESTSuite中。
  • unittest.defaultTestLoader:等於unittest.TestLoader()
  • unittest.TestProgram:TestProgram類名被賦值給了main變量,然后通過unittest.main()的形式調用。

歡迎斧正,that's all see also:[python 單元測試(unittest)](https://www.cnblogs.com/wangsen-123/p/9098555.html)| [Python單元測試unittest](https://www.cnblogs.com/feng0815/p/8045850.html) | [單元測試]( ) | [官網:unittest單元測試框架](https://docs.python.org/zh-cn/3/library/unittest.html#module-unittest) | [unittest的discover方法使用](https://www.cnblogs.com/NancyRM/p/8378199.html) | [python unittest.TestLoader()類的幾種尋找testcase的方法的使用](https://blog.csdn.net/u010895119/article/details/79164536) | [HTMLTestRunner 漢化版,同時支持python 2和3,增加截圖展示功能,失敗重試](https://github.com/GoverSky/HTMLTestRunner_cn)


免責聲明!

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



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