unittest --- 單元測試框架

原始碼:Lib/unittest/__init__.py


(假如你已經熟悉相關基礎的測試概念,你可能會希望跳過以下段落,直接參考 assert 方法清單。)

unittest 原生的單元測試框架最初由 JUnit 開發,和其他程式語言相似有主要的單元測試框架。支援自動化測試,對測試分享安裝與關閉程式碼,集合所有匯總的測試,並且獨立各個測試報告框架。

unittest 用來作為實現支援一些重要的物件導向方法的概念:

test fixture

一個 test fixture 代表執行一個或多個測試所需要的準備,以及其他相關清理操作,例如可以是建立臨時性的或是代理用 (proxy) 資料庫、目錄、或是啟動一個伺服器程序。

test case(測試用例)

一個 test case 是一個獨立的單元測試。這是用來確認一個特定設定的輸入的特殊回饋。unittest 提供一個基礎類別,類別 TestCase,可以用來建立一個新的測試條例。

test suite(測試套件)

test suite 是一個搜集測試條例,測試套件,或是兩者皆有。它需要一起被執行並用來匯總測試。

test runner(測試執行器)

test runner 是一個編排測試執行與提供結果給使用者的一個元件。執行器可以使用圖形化介面,文字介面或是回傳一個特別值用來標示出執行測試的結果。

也參考

doctest 模組

另一個執行測試的模組,但使用不一樣的測試方法與規範。

Simple Smalltalk Testing: With Patterns

Kent Beck 的原始論文討論使用 unittest 這樣模式的測試框架。

pytest

第三方的單元測試框架,但在撰寫測試時使用更輕量的語法。例如: assert func(10) == 42

The Python Testing Tools Taxonomy

一份詳細的 Python 測試工具列表,包含 functional testing 框架和mock object 函式庫。

Testing in Python Mailing List

一個專門興趣的群組用來討論 Python 中的測試方式與測試工具。

The script Tools/unittestgui/unittestgui.py in the Python source distribution is a GUI tool for test discovery and execution. This is intended largely for ease of use for those new to unit testing. For production environments it is recommended that tests be driven by a continuous integration system such as Buildbot, Jenkins, GitHub Actions, or AppVeyor.

簡單範例

unittest 模組提供一系列豐富的工具用來建構與執行測試。本節將展示這一系列工具中一部份,它們已能滿足大部份使用者需求。

這是一段簡短的腳本用來測試 3 個字串方法:

import unittest

class TestStringMethods(unittest.TestCase):

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

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

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

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

測試用例 (test case) 可以透過繼承 unittest.TestCase 類別來建立。這裡定義了三個獨立的物件方法,名稱皆以 test 開頭。這樣的命名方式能告知 test runner 哪些物件方法為定義的測試。

每個測試的關鍵為呼叫 assertEqual() 來確認是否為期望的結果; assertTrue() 或是 assertFalse() 用來驗證一個條件式; assertRaises() 用來驗證是否觸發一個特定的 exception。使用這些物件方法來取代 assert 陳述句,將能使 test runner 收集所有的測試結果並產生一個報表。

The setUp() and tearDown() methods allow you to define instructions that will be executed before and after each test method. They are covered in more detail in the section Organizing test code.

最後將顯示一個簡單的方法去執行測試 unittest.main() 提供一個命令列介面測試腳本。當透過命令列執行,輸出結果將會像是:

...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

在測試時加入 -v 選項將指示 unittest.main() 提高 verbosity 層級,產生以下的輸出:

test_isupper (__main__.TestStringMethods.test_isupper) ... ok
test_split (__main__.TestStringMethods.test_split) ... ok
test_upper (__main__.TestStringMethods.test_upper) ... ok

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

OK

以上的例子顯示大多數使用 unittest 特徵足以滿足大多數日常測試的需求。接下來第一部分文件的剩餘部分將繼續探索完整特徵設定。

在 3.11 版的變更: The behavior of returning a value from a test method (other than the default None value), is now deprecated.

命令列介面

單元測試模組可以透過命令列執行測試模組,物件甚至個別的測試方法:

python -m unittest test_module1 test_module2
python -m unittest test_module.TestClass
python -m unittest test_module.TestClass.test_method

你可以透過一個串列與任何模組名稱的組合,完全符合類別與方法的名稱。

測試模組可以根據檔案路徑指定:

python -m unittest tests/test_something.py

這允許你使用 shell 檔案名稱補完功能 (filename completion) 來指定測試模組。給定的檔案路徑必須亦能被當作模組 import。此路徑轉換為模組名稱的方式為移除 '.py' 並將路徑分隔符 (path separator) 轉換成 '.'。 假如你的測試檔案無法被 import 成模組,你應該直接執行該測試檔案。

透過增加 -v 的旗標數,可以在你執行測試時得到更多細節(更高的 verbosity):

python -m unittest -v test_module

若執行時不代任何引數,將執行 Test Discovery(測試探索)

python -m unittest

列出所有命令列選項:

python -m unittest -h

在 3.2 版的變更: 在早期的版本可以個別執行測試方法和不需要模組或是類別。

在 3.14 版被加入: Output is colorized by default and can be controlled using environment variables.

命令列模式選項

unittest 支援以下命令列選項:

-b, --buffer

Standard output 與 standard error stream 將在測試執行被緩衝 (buffer)。這些輸出在測試透過時被丟棄。若是測試錯誤或失則,這些輸出將會正常地被印出,並且被加入至錯誤訊息中。

-c, --catch

Control-C 測試執行過程中等待正確的測試結果並回報目前為止所有的測試結果。第二個 Control-C 拋出一般例外 KeyboardInterrupt

參照 Signal Handling 針對函式提供的功能。

-f, --failfast

在第一次錯誤或是失敗停止執行測試。

-k

Only run test methods and classes that match the pattern or substring. This option may be used multiple times, in which case all test cases that match any of the given patterns are included.

Patterns that contain a wildcard character (*) are matched against the test name using fnmatch.fnmatchcase(); otherwise simple case-sensitive substring matching is used.

Patterns are matched against the fully qualified test method name as imported by the test loader.

For example, -k foo matches foo_tests.SomeTest.test_something, bar_tests.SomeTest.test_foo, but not bar_tests.FooTest.test_something.

--locals

透過 traceback 顯示本地變數。

--durations N

Show the N slowest test cases (N=0 for all).

在 3.2 版被加入: 增加命令列模式選項 -b-c-f

在 3.5 版被加入: 命令列選項 --locals

在 3.7 版被加入: 命令列選項 -k

在 3.12 版被加入: 命令列選項 --durations

對執行所有的專案或是一個子集合測試,命令列模式可以可以被用來做測試探索。

Test Discovery(測試探索)

在 3.2 版被加入.

單元測試支援簡單的 test discovery(測試探索)。為了相容於測試探索,所有的測試檔案都要是模組或是套件,並能從專案的最上層目錄中 import(代表它們的檔案名稱必須是有效的 identifiers)。

Test discovery(測試探索)實作在 TestLoader.discover(),但也可以被用於命令列模式。基本的命令列模式用法如下:

cd project_directory
python -m unittest discover

備註

python -m unittest 作為捷徑,其功能相當於 python -m unittest discover。假如你想傳遞引數至探索測試的話,一定要明確地加入 discover 子指令。

discover 子命令有以下幾個選項:

-v, --verbose

詳細 (verbose) 輸出

-s, --start-directory directory

開始尋找的資料夾(預設為 .

-p, --pattern pattern

匹配測試檔案的模式(預設為 test*.py

-t, --top-level-directory directory

專案的最高階層目錄(預設為開始的資料夾)

-s, -p, 和 -t 選項依照傳遞位置作為引數排序順序。以下兩個命令列被視為等價:

python -m unittest discover -s project_directory -p "*_test.py"
python -m unittest discover project_directory "*_test.py"

As well as being a path it is possible to pass a package name, for example myproject.subpackage.test, as the start directory. The package name you supply will then be imported and its location on the filesystem will be used as the start directory.

警示

Test discovery loads tests by importing them. Once test discovery has found all the test files from the start directory you specify it turns the paths into package names to import. For example foo/bar/baz.py will be imported as foo.bar.baz.

If you have a package installed globally and attempt test discovery on a different copy of the package then the import could happen from the wrong place. If this happens test discovery will warn you and exit.

If you supply the start directory as a package name rather than a path to a directory then discover assumes that whichever location it imports from is the location you intended, so you will not get the warning.

Test modules and packages can customize test loading and discovery by through the load_tests protocol.

在 3.4 版的變更: Test discovery supports namespace packages.

在 3.11 版的變更: Test discovery dropped the namespace packages support. It has been broken since Python 3.7. Start directory and its subdirectories containing tests must be regular package that have __init__.py file.

If the start directory is the dotted name of the package, the ancestor packages can be namespace packages.

在 3.14 版的變更: Test discovery supports namespace package as start directory again. To avoid scanning directories unrelated to Python, tests are not searched in subdirectories that do not contain __init__.py.

Organizing test code

The basic building blocks of unit testing are test cases --- single scenarios that must be set up and checked for correctness. In unittest, test cases are represented by unittest.TestCase instances. To make your own test cases you must write subclasses of TestCase or use FunctionTestCase.

The testing code of a TestCase instance should be entirely self contained, such that it can be run either in isolation or in arbitrary combination with any number of other test cases.

The simplest TestCase subclass will simply implement a test method (i.e. a method whose name starts with test) in order to perform specific testing code:

import unittest

class DefaultWidgetSizeTestCase(unittest.TestCase):
    def test_default_widget_size(self):
        widget = Widget('The widget')
        self.assertEqual