Python unittest 模組教學 part 2

Last updated on  Mar 20, 2023  in  Python 程式設計 - 中階  by  Amo Chen  ‐ 4 min read

本文是 Python unittest 模組的教學:

本文繼續介紹 unittest 幾個重要的方法,諸如 tearDown, test discover 以及 function test case 等等。

測試也需要善後工作- tearDown

測試過程難免會需要一些 test fixture, 例如產生設定檔、連結資料庫、產生檔案等等,而這些 test fixture 也可能需要在測試完成後進行善後與清除,此時可以選擇實作 unittest 模組的 tearDown 方法。

tearDown 會在每一個測試方法執行完之後被呼叫( setUp 則相反),因此可以把測試的善後與清除的工作放在 tearDown 方法中(不過也有人將測試過程的錯誤訊息在此方法中一併顯示),詳情可以直接參照以下範例 1。

記得,unittest 模組每一次執行測試方法的 順序setUp > test 開頭的測試方法 > tearDown

範例 1:

import unittest


def compare(v1, v2):
    if v1 == v2:
        return True
    return False


class MyTest(unittest.TestCase):
    def setUp(self):
        print("setUp!")
        self.greeting = "Hello"
        self.test_string = "HellO"
        self.test_byte_string = b"\x48\x65\x6c\x6c\x6f"
        self.v1, self.v2 = (1, True)

    def test_compare_string(self):
        print("test_compare_string")
        assert compare(self.greeting, self.test_string) is False

    def test_compare_byte_string(self):
        print("test_compare_byte_string")
        assert compare(self.greeting, self.test_byte_string) is False

    def test_compare_int_bool(self):
        print("test_compare_int_bool")
        assert compare(self.v1, self.v2) is True

    def tearDown(self):
        print("tearDown!")


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

範例 1 執行結果:

setUp!
test_compare_byte_string
tearDown!
.setUp!
test_compare_int_bool
tearDown!
.setUp!
test_compare_string
tearDown!
.
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

從上述執行結果可以看到 setUp 與 tearDown 確實會在每個 test case 執行之前與之後執行。

Test Discover

範例 2 中提及利用 test suite 對測試進行分類管理,除了能將性質相似的 test case 放在同 1 個類別或檔案之外,也可以將這些測試程式碼以資料夾進行分類管理,再搭配 unittest 的 test discover 功能,就能夠執行指定的資料夾內的所有測試程式。

首先,假設我們建立了一個專案資料夾為 MyProject ,接著在這資料夾內建立了一個 tests 資料夾, tests 資料夾內有兩個 test case 以及一個 __init__.py, 資料夾的結構如下所示:

MyProject/
    tests/
        Case1_tests.py
        Case2_tests.py
        __init__.py

接下來,我們能夠使用以下指令讓 Python 自動載入 test case 並且執行:

$ python -m unittest discover -t /path/to/MyProject -s ./tests/ -p '*_tests.py'

上述指令的意思代表在 MyProject 資料夾載入 tests 資料夾內所有檔案以 _tests.py 結尾的 test case 並執行測試。

這就是 unittest 的 test discover 功能!

舊有的 test case 回收再利用

有些時候可能會遇到專案已經有一些不是用 class 寫成的測試,例如:

def test_compare_string():
    assert compare(greeting, test) is False

在這種情況下我們也不需要捨棄這些測式函式(當然,有時間的話,轉為類別會是 Python 官方比較推薦的方式), Python 針對這種情況也設計了 FunctionTestCase 類別,讓我們將這些既有的測試函式轉換為 TestCase 類別。

範例 2 利用 FunctionTestCase 類別將舊有的測試函式轉換為 TestCase 類別後進行測試。

範例 2:

import unittest


def compare(v1, v2):
    if v1 == v2:
        return True
    return False


def setUp():
    global v1, v2
    global greeting
    global test_string
    global test_byte_string
    greeting = "Hello"
    test_string = u"HellO"
    test_byte_string = b"\x48\x65\x6c\x6c\x6f"
    v1, v2 = (1, True)


def test_compare_string():
    assert compare(greeting, test_string) is False


def test_compare_byte_string():
    assert compare(greeting, test_byte_string) is False


def test_compare_int_bool():
    assert compare(v1, v2) is True


def test_compare_type():
    assert compare(v1, v2) is True


def tearDown():
    greeting = None
    test_string = None
    test_byte_string = None
    v1, v2 = (None, None)


if __name__ == '__main__':
    testcases = (
      test_compare_string,
      test_compare_byte_string,
      test_compare_int_bool,
      test_compare_type,
    )
    for test in testcases:
        testcase = unittest.FunctionTestCase(test, setUp=setUp, tearDown=tearDown)
        unittest.TextTestRunner(verbosity=2).run(testcase)

從上述範例中的 unittest.FunctionTestCase(test, setUp=setUp, tearDown=tearDown) 可以發現 FunctionTestCase 也支援 setUp 以及 tearDown 的用法。

範例 2 執行結果如下:

unittest.case.FunctionTestCase (test_compare_string) ... ok

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

OK
unittest.case.FunctionTestCase (test_compare_byte_string) ... ok

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

OK
unittest.case.FunctionTestCase (test_compare_int_bool) ... ok

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

OK
unittest.case.FunctionTestCase (test_compare_type) ... ok

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

OK

雖然範例 2 的用法看似方便,但事實上並不是一個被 Python 官方推薦的方法, 應盡量避免使用 FunctionTestCase

略過測試(Skipping tests) & 預期中的錯誤(Expected failures)

Python 3.1 之後,在 unittest 模組也增加了 skipping tests 跟 expected failures 的功能。

Skipping tests 可以針對測試方法的執行進行條件的設定,例如滿足特定條件就略過測試。

Expected failures 則可以使用在某些預期必定會發生錯誤的測試方法上,例如測試故意打錯帳號密碼,一定要能夠阻擋登入,要是通過就有鬼了。

範例 3 展示了 skipping tests 跟 expected failures 的方法:

import sys
import unittest


mylib_version  = (1, 3)


class MyTestCase(unittest.TestCase):
    @unittest.skip("demonstrating skipping")
    def test_nothing(self):
        self.fail("shouldn't happen")

    @unittest.skipIf(mylib_version < (1, 3),
                     "not supported in this library version")
    def test_format(self):
        # Tests that work for only a certain version of the library.
        pass

    @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
    def test_windows_support(self):
        # windows specific testing code
        pass


@unittest.skipIf(mylib_version > (1, 2),  "not supported in this library version")
class MySkippedClass(unittest.TestCase):
    def test_nothing(self):
        pass


class ExpectedFailureTestCase(unittest.TestCase):
    @unittest.expectedFailure
    def failure_test(self):
        self.assertEqual(1, 0, "broken")


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

範例 3 執行結果(s 代表 skipped):

s.ss
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK (skipped=3)

範例 3 的重點在於對於想要設定 skipping 或者 expected failure 的測試方法可以使用 Python 裝飾子(decorator)的方式進行設定,相當簡單易懂。

最後我們在範例 4 中展示如何利用 unittest.skip 客製化自己的 skipping 的裝飾子(decorator),

範例 4:

import sys
import unittest


class MyClass(object):
    do_test = True


def skipUnlessHasattr(obj, attr):
    if hasattr(obj, attr):
        return lambda func: func
    return unittest.skip("{!r} doesn't have {!r}".format(obj, attr))


@skipUnlessHasattr(MyClass, "test")
class MySkippedClass(unittest.TestCase):
    def test_nothing(self):
        pass


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

範例 4 的 skipUnlessHasattr 函式利用回傳 unittest.skip 來達成客製化 Skipping 裝飾子的效果。

總結

以上就是本篇 unittest 模組的大致教學與使用範例,在學會使用 unittest 之後,就可以學著開始利用一些以 unittest 模組為基礎而開發的測試套件(例如 nose, pytest 等等),相信一定可以駕輕就熟。

References

http://docs.python.org/3/library/unittest.html

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!