unittest --- 單元測試框架¶
(假如你已經熟悉相關基礎的測試概念,你可能會希望跳過以下段落,直接參考 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 usingfnmatch.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 foomatchesfoo_tests.SomeTest.test_something,bar_tests.SomeTest.test_foo, but notbar_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(widget.size(), (50, 50))
Note that in order to test something, we use one of the assert* methods
provided by the TestCase base class. If the test fails, an
exception will be raised with an explanatory message, and unittest
will identify the test case as a failure. Any other exceptions will be
treated as errors.
Tests can be numerous, and their set-up can be repetitive. Luckily, we
can factor out set-up code by implementing a method called
setUp(), which the testing framework will automatically
call for every single test we run:
import unittest
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget('The widget')
def test_default_widget_size(self):
self.assertEqual(self.widget.size(), (50,50),
'incorrect default size')
def test_widget_resize(self):
self.widget.resize(100,150)
self.assertEqual(self.widget.size(), (100,150),
'wrong size after resize')
備註
The order in which the various tests will be run is determined by sorting the test method names with respect to the built-in ordering for strings.
If the setUp() method raises an exception while the test is
running, the framework will consider the test to have suffered an error, and
the test method will not be executed.
Similarly, we can provide a tearDown() method that tidies up
after the test method has been run:
import unittest
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget('The widget')
def tearDown(self):
self.widget.dispose()
If setUp() succeeded, tearDown() will be
run whether the test method succeeded or not.
Such a working environment for the testing code is called a
test fixture. A new TestCase instance is created as a unique
test fixture used to execute each individual test method. Thus
setUp(), tearDown(), and TestCase.__init__()
will be called once per test.
It is recommended that you use TestCase implementations to group tests together
according to the features they test. unittest provides a mechanism for
this: the test suite, represented by unittest's
TestSuite class. In most cases, calling unittest.main() will do
the right thing and collect all the module's test cases for you and execute
them.
However, should you want to customize the building of your test suite, you can do it yourself:
def suite():
suite = unittest.TestSuite()
suite.addTest(WidgetTestCase('test_default_widget_size'))
suite.addTest(WidgetTestCase('test_widget_resize'))
return suite
if __name__ == '__main__':
runner = unittest.TextTestRunner()
runner.run(suite())
You can place the definitions of test cases and test suites in the same modules
as the code they are to test (such as widget.py), but there are several
advantages to placing the test code in a separate module, such as
test_widget.py:
The test module can be run standalone from the command line.
The test code can more easily be separated from shipped code.
There is less temptation to change test code to fit the code it tests without a good reason.
Test code should be modified much less frequently than the code it tests.
Tested code can be refactored more easily.
Tests for modules written in C must be in separate modules anyway, so why not be consistent?
If the testing strategy changes, there is no need to change the source code.
Re-using old test code¶
Some users will find that they have existing test code that they would like to
run from unittest, without converting every old test function to a
TestCase subclass.
For this reason, unittest provides a FunctionTestCase class.
This subclass of TestCase can be used to wrap an existing test
function. Set-up and tear-down functions can also be provided.
Given the following test function:
def testSomething():
something = makeSomething()
assert something.name is not None
# ...
one can create an equivalent test case instance as follows, with optional set-up and tear-down methods:
testcase = unittest.FunctionTestCase(testSomething,
setUp=makeSomethingDB,
tearDown=deleteSomethingDB)
備註
Even though