C語言中文網 目錄

Python unittest(PyUnit)單元測試框架完全攻略(看了無師自通)

< 上一頁Python doctest Python TestSuite下一頁 >

PyUnit(unittest) 是 Python 自帶的單元測試框架,用于編寫和運行可重復的測試。PyUnit 是 xUnit 體系的一個成員,xUnit 是眾多測試框架的總稱,PyUnit 主要用于進行白盒測試和回歸測試。

如果你使用的是 2.1 或更早版本的 Python,則可能需要自行下載和安裝 PyUnit,現在的開發者通常不需要操心這些事情。

通過 PyUnit 可以讓測試具有持久性,測試與開發同步進行,測試代碼與開發代碼一同發布。使用 PyUnit 具有如下好處:
  • 可以使測試代碼與產品代碼分離。
  • 針對某一個類的測試代碼只需要進行較少的改動,便可以應用于另一個類的測試。
  • PyUnit 開放源代碼,可以進行二次開發,方便對 PyUnit 的擴展。

PyUnit 是一個簡單、易用的測試框架,其具有如下特征:
  • 使用斷言方法判斷期望值和實際值的差異,返回 bool 值。
  • 測試驅動設備可使用共同的初始化變量或實例。
  • 測試包結構便于組織和集成運行。

PyUnit (unittest) 的用法

所有測試的本質其實都是一樣的,都是通過給定參數來執行函數,然后判斷函數的實際輸出結果和期望輸出結果是否一致。

PyUnit 測試與其他 xUnit 的套路一樣,基于斷言機制來判斷函數或方法的實際輸出結果和期望輸出結果是否一致,測試用例提供參數來執行函數或方法,獲取它們的執行結果,然后使用斷言方法來判斷該函數或方法的輸出結果與期望輸出結果是否一致,如果一致則說明測試通過;如果不一致則說明測試不通過。

目前還有一種流行的開發方式叫作測試驅動開發,這種方式強調先編寫測試用例,然后再編寫函數和方法。假如程序要開發滿足 A 功能的 fun_a() 函數,采用測試驅動開發的步驟如下:
  1. 為 fun_a() 函數編寫測試用例,根據業務要求,使用大量不同的參數組合來執行 fun_a() 函數,并斷言該函數的執行結果與業務期望的執行結果匹配。
  2. 編寫、修改 fun_a() 函數。
  3. 運行 fun_a() 函數的測試用例,如果測試用例不能完全通過;則重復第 2 步和第 3 步,直到 fun_a() 的所有測試用例全部通過。

測試驅動開發強調結果導向,也就是在開發某個功能之前,先定義好該功能的最終結果(測試用例關注函數的執行結果),然后再去開發該功能。就像建筑工人在砌墻之前,要先拉好一根筆直的繩子(作用相當于測試用例),然后再開始砌墻,這樣砌出來的墻就會符合標準。所以說測試驅動開發確實是一種不錯的開發方式。

下面開發一個簡單的 fk_math.py 程序,該程序包含兩個函數,分別用于計算一元一次方程的解和二元一次方程的解。
def one_equation(a , b):
    '''
    求一元一次方程a * x + b = 0的解
    參數a - 方程中變量的系數
    參數b - 方程中的常量
    返回 方程的解
    '''
    # 如果a = 0,則方程無法求解
    if a == 0:
        raise ValueError("參數錯誤")
    # 返回方程的解
    else:
#        return -b / a  # ①
        return b / a
def two_equation(a , b , c):
    '''
    求一元二次方程a * x * x + b * x + c = 0的解
    參數a - 方程中變量二次冪的系數
    參數b - 方程中變量的系數
    參數c - 方程中的常量
    返回 方程的根
    '''
    # 如果a == 0,變成一元一次方程
    if a == 0:
        raise ValueError("參數錯誤")
    # 有理數范圍內無解
    elif b * b - 4 * a * c < 0:
        raise ValueError("方程在有理數范圍內無解")
    # 方程有唯一的解
    elif b * b - 4 * a * c == 0:
        # 使用數組返回方程的解
        return -b / (2 * a)
    # 方程有兩個解
    else:
        r1 = (-b + (b * b - 4 * a * c) ** 0.5) / 2 / a
        r2 = (-b - (b * b - 4 * a * c) ** 0.5) / 2 / a
        # 方程的兩個解
        return r1, r2
在定義好上面的 tk_math.py 程序之后,該程序就相當于一個模塊,接下來為該模塊編寫單元測試代碼。

unittest 要求單元測試類必須繼承 unittest.TestCase,該類中的測試方法需要滿足如下要求:
  • 測試方法應該沒有返回值。
  • 測試方法不應該有任何參數。
  • 測試方法應以test 開頭。

下面是測試用例的代碼:
import unittest

from fk_math import *

class TestFkMath(unittest.TestCase):
    # 測試一元一次方程的求解
    def test_one_equation(self):
        # 斷言該方程求解應該為-1.8
        self.assertEqual(one_equation(5 , 9) , -1.8)
        # 斷言該方程求解應該為-2.5
        self.assertTrue(one_equation(4 , 10) == -2.5 , .00001)
        # 斷言該方程求解應該為27/4
        self.assertTrue(one_equation(4 , -27) == 27 / 4)
        # 斷言當a == 0時的情況,斷言引發ValueError
        with self.assertRaises(ValueError):
            one_equation(0 , 9)
    # 測試一元二次方程的求解
    def test_two_equation(self):
        r1, r2 = two_equation(1 , -3 , 2)
        self.assertCountEqual((r1, r2), (1.0, 2.0), '求解出錯')
        r1, r2 = two_equation(2 , -7 , 6)
        self.assertCountEqual((r1, r2), (1.5, 2.0), '求解出錯')
        # 斷言只有一個解的情形
        r = two_equation(1 , -4 , 4)
        self.assertEqual(r, 2.0, '求解出錯')
        # 斷言當a == 0時的情況,斷言引發ValueError
        with self.assertRaises(ValueError):
            two_equation(0, 9, 3)
        # 斷言引發ValueError
        with self.assertRaises(ValueError):
            two_equation(4, 2, 3)
上面測試用例中使用斷言方法判斷函數的實際輸出結果與期望輸出結果是否一致,如果一致則表明測試通過,否則表明測試失敗。

在上面的測試用例中,在測試 one_equation() 方法時傳入了四組參數。至于此處到底需要傳入幾組參數進行測試,關鍵取決于測試者要求達到怎樣的邏輯覆蓋程度,隨著測試要求的提高,此處可能需要傳入更多的測試參數。當然,此處只是介紹 PyUnit 的用法示例,并未刻意去達到怎樣的邏輯覆蓋,這一點請務必留意。

在測試某個方法時,如果實際測試要求達到某種覆蓋程度,那么在編寫測試用例時必須傳入多組參數來進行測試,使得測試用例能達到指定的邏輯覆蓋。

unittest.TestCase 內置了大量 assertXxx 方法來執行斷言,其中最常用的斷言方法如表 1 所示。

表 1 TestCase 中最常用的斷言方法
斷言方法 檢查條件
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertlsInstance(a, b) isinstance(a, b)
assertNotIsInstance(a, b) not isinstance(a, b)

除了上面這些斷言方法,如果程序要對異常、錯誤、警告和日志進行斷言判斷,TestCase 提供了如表 2 所示的斷言方法。

表 2 TestCase 包含的與異常、錯誤、警告和日志相關的斷言方法
斷言方法 檢查條件
assertRaises(exc, fun, *args, **kwds) fun(*args, **kwds) 引發 exc 異常
assertRaisesRegex(exc, r, fun, *args, **kwds) fun(*args, **kwds) 引發 exc 異常,且異常信息匹配 r 正則表達式
assertWarns(warn, fun, *args, **kwds) fun(*args, **kwds) 引發 warn 警告
assertWamsRegex(warn, r, fun, *args, **kwds) fun(*args, **kwds) 引發 warn 警告,且警告信息匹配 r 正則表達式
assertLogs(logger, level) With 語句塊使用日志器生成 level 級別的日志

TestCase 還包含了如表 3 所示的斷言方法用于完成某種特定檢查。

表 3 TestCase 包含的用于完成某種特定檢查的斷言方法
斷言方法 檢查條件
assertAlmostEqual(a, b) round(a-b, 7) == 0
assertNotAlmostEqual(a, b) round(a-b, 7) != 0
assertGreater(a, b) a > b
assertGreaterEqual(a, b) a >= b
assertLess(a, b) a < b
assertLessEqual(a, b) a <= b
assertRegex(s, r) r.search(s)
assertNotRegex(s, r) not r.search(s)
assertCountEqual(a, b) a、b 兩個序列包含的元素相同,不管元素出現的順序如何

當測試用例使用 assertEqual() 判斷兩個對象是否相等時,如果被判斷的類型是字符串、序列、列表、元組、集合、字典,則程序會自動改為使用如表 4 所示的斷言方法進行判斷。換而言之,如表 4 所示的斷言方法其實沒有必要使用,unittest 模塊會自動應用它們。

表 4 TestCase 包含的針對特定類型的斷言方法
斷言方法 用于比較的類型
assertMultiLineEqual(a, b) 字符串(string)
assertSequenceEqual(a, b) 序列(sequence)
assertListEqual(a, b) 列表(list)
assertTupleEqual(a, b) 元組(tuple)
assertSetEqual(a, b) 集合(set 或 frozenset)
assertDictEqual(a, b) 字典(dict)

運行測試

在編寫完測試用例之后,可以使用如下兩種方式來運行它們:
  1. 通過代碼調用測試用例。程序可以通過調用 unittest.main() 來運行當前源文件中的所有測試用例。例如,在上面的測試用例中增加如下代碼:

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

  2. 使用 unittest 模塊運行測試用例。使用該模塊的語法格式如下:

    python -m unittest 測試文件

    在使用 python -m unittest 命令運行測試用例時,如果沒有指定測試用例,該命令將自動查找并運行當前目錄下的所有測試用例。因此,程序也可直接使用如下命令來運行所有測試用例:

    py -m unittest


采用上面任意一種方式來運行測試用例,均可以看到如下輸出結果:

..
Ran 2 tests in 0.000s

OK

在上面輸出結果的第一行可以看到兩個點,這里的每個點都代表一個測試用例(每個以 test_ 開頭的方法都是一個真正獨立的測試用例)的結果。由于上面測試類中包含了兩個測試用例,因此此處看到兩個點,其中點代表測試用例通過。此處可能出現如下字符:
  • .:代表測試通過。
  • F:代表測試失敗,F 代表 failure。
  • E:代表測試出錯,E 代表 error。
  • s:代表跳過該測試,s 代表 skip。

在上面輸出結果的橫線下面看到了“Ran 2 tests in O.OOOs”提示信息,這行提示信息說明本次測試運行了多少個測試用例。如果看到下面提示 OK,則表明所有測試用例均通過。

上面的測試用例都可通過,是因為 fk_math.py 程序沒有錯誤。如果將 fk_math.py 程序中的 ① 號代碼故意修改為出錯,假如將 ① 號代碼修改為 return b/a,再次運行上面的測試用例,將會看到如下輸出結果:

F.
======================================================================
FAIL: test_one_equation (__main__.TestFkMath)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "E:\test_fk_math.py", line 24, in test_one_equation
    self.assertEqual(one_equation(5 , 9) , -1.8)
AssertionError: 1.8 != -1.8
----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

此時看到第一行的輸出信息為 F,這表明第一個測試用例失敗,第二個測試用例成功。

接下來在兩條橫線之間可以看到斷言錯誤的 Traceback 信息,以及函數運行的實際輸出結果和期望輸出結果的差異。信息提示該函數運行的實際輸出結果是 1.8,但期望輸出結果是 -1.8。
< 上一頁Python doctest Python TestSuite下一頁 >

精美而實用的網站,提供C語言、C++、STL、Linux、Shell、Java、Go語言等教程,以及socket、GCC、vi、Swing、設計模式、JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo