來,賜座,上華子!一起聊聊 Python 的單元測試框架(二):nose 和它的繼任者 nose2

2020-08-14 15:21:05

一、nose

nose 是一個第三方單元測試框架,它完全相容 unittest,並且號稱是一個更好用的測試框架。

那麼 nose 除了具備 unittest 的所有功能外,還具有哪些優勢呢?

1.1 用例編寫

用例的編寫方式除了編寫繼承於 unittest.TestCase 的測試類外,還可以編寫成沒有繼承的測試類。比如,寫成如下形式也會被 nose 視作一個測試類:

from nose.tools import raises

class TestStringMethods:

    def test_upper(self):
        assert 'foo'.upper() == 'FOO'

    def test_isupper(self):
        assert 'FOO'.isupper()
        assert not 'Foo'.isupper()

    @raises(TypeError)
    def test_split(self):
        s = 'hello world'
        assert s.split() == ['hello', 'world']
        # check that s.split fails when the separator is not a string
        s.split(2)
複製程式碼

當然,測試類並沒有繼承 unittest.TestCase,將不能使用其內建的各類 assertXXX 方法,進而導致用例出錯時無法獲得更加詳細的上下文資訊。

此外,nose 也支援定義函數來作爲測試,這給許多簡單的測試場景帶來很大的便利:

def test_upper():
    assert 'foo'.upper() == 'FOO'
複製程式碼

1.2 用例發現和執行

unittest 所支援的用例發現和執行能力,nose 均支援。 nose 支援用例自動(遞回)發現:

  • 預設發現當前目錄下所有包含 test 的測試用例,但不包括以 _ 開頭的用例
    • 使用 nosetests 命令
  • 通過 -w 參數指定要自動發現的目錄, -m 參數指定用例檔案、目錄、函數、類的名稱模式(正則匹配)
    • nosetests -w project_directory "test_.+"

nose 也支援執行指定用例:

  • 指定測試模組
    • nosetests test.module
  • 指定測試類
    • nosetests a.test:TestCase
  • 指定測試方法
    • nosetests another.test:TestCase.test_method
  • 指定測試檔案路徑
    • nosetests /path/to/test/file.py
  • 指定測試檔案路徑+測試類或測試函數(這是 unittest 所不支援的)
    • nosetests /path/to/test/file.py:TestCase
    • nosetests /path/to/test/file.py:TestCase.test_method
    • nosetests /path/to/test/file.py:test_function

1.3 測試夾具(Fixtures)

nose 除了支援 unittest 所支援的定義測試前置和清理方式,還支援一種更爲簡單的定義方式:

def setup_func():
    "set up test fixtures"

def teardown_func():
    "tear down test fixtures"

@with_setup(setup_func, teardown_func)
def test():
    "test ..."
複製程式碼

只需定義兩個函數用來表示前置和清理方法,通過 nose.tools.with_setup 裝飾器裝飾測試函數,nose 便會在執行測試用例前後分別執行所定義的前置和清理函數。

1.4 子測試/測試生成器

nose 除了支援 unittest 中的 TestCase.subTest,還支援一種更爲強大的子測試編寫方式,也就是 測試生成器(Test generators),通過 yield 實現。

在下面 下麪的範例中,定義一個 test_evens 測試函數,裏面生成了 5 個子測試 check_even

def test_evens():
    for i in range(0, 5):
        yield check_even, i, i*3

def check_even(n, nn):
    assert n % 2 == 0 or nn % 2 == 0
複製程式碼

此外,相較於 unittest.TestCase.subTest 多個子測試只能執行一次測試前置和清理,nose 的 測試生成器 可以支援每個子測試執行一次測試前置和清理,如:

def test_generator():
    # ...
    yield func, arg, arg # ...

@with_setup(setup_func, teardown_func)
def func(arg):
    assert something_about(arg)
複製程式碼

1.5 外掛體系

nose 相較於 unittest 一個最大的優勢就是外掛體系,自帶了很多有用的外掛,也有豐富的第三方外掛。這樣就能做更多的事情。

其中,自帶外掛如下:

  • AllModules:在所有模組中收集用例
  • Attrib:給用例打標籤,並可執行含指定標籤的用例
  • Capture:捕獲用例的標準輸出
  • Collect:快速收集用例
  • Cover:統計程式碼覆蓋率
  • Debug:用例失敗時進入 pdb 偵錯
  • Deprecated:標記用例爲棄用
  • Doctests:執行文件用例
  • Failure Detail:斷言失敗時提供上下文資訊
  • Isolate:保護用例避免受一些副作用的影響
  • Logcapture:捕捉 logging 輸出
  • Multiprocess:並行執行用例
  • Prof:使用熱點分析器進行分析
  • Skip:標記用例爲跳過
  • Testid:爲輸出的每個用例名稱新增測試 ID
  • Xunit:以 xunit 格式輸出測試結果

而第三方庫則多種多樣,如用來生成 HTML 格式測試報告的 nose-htmloutput 等,這裏不再一一列出。

得益於 nose 豐富的外掛生態,當 nose 本身不能夠完全滿足我們的測試需求時,可以通過安裝外掛,並在 nosetests 命令列指定該外掛所提供的特定參數即可非常容易的使用外掛。 相較於 unittest,就能省去很多自己開發額外測試邏輯的精力。

二、nose2(免費領取Python自動化學習資料  工具,面試寶典面試技巧,加QQ羣,785128166,羣內還會大佬技術交流

nose2 是 nose 的繼任者。 它們的理念都是讓編寫和執行測試用例變得更容易。

它們有很多相同點,比如都相容 unittest,支援使用函數作爲測試用例,支援子測試,擁有外掛體系。但也有很多不同點,下面 下麪列出一些主要的不同點:

  • 發現和載入測試
    • nose 自行實現了模組載入功能,使用惰性方式載入測試模組,載入一個執行一個。
    • nose2 則藉助內建的 import() 匯入模組,並且是先全部載入,再執行用例
    • nose2 並不支援 nose 所支援的所有測試用例專案結構,比如如下用例檔案的結構在 nose2 中就不受支援:
.
`-- tests
    |-- more_tests
    |   `-- test.py
    `-- test.py
複製程式碼
  • 測試前置和清理函數級別
    • nose 支援方法、類、模組和包級別的測試前置和清理函數
    • nose2 則不支援包級別的測試前置和清理函數
  • 子測試
    • nose2 除了支援使用測試生成器來實現子測試外,還支援使用參數化測試(Parameterized tests)來實現子測試
    • nose2 除了像 nose 一樣支援在測試函數和測試類(不繼承於 unittest.TestCase)中支援參數化測試和測試生成器外,還支援在繼承於 unittest.TestCase 的測試類中使用
  • 設定化
    • nose 期望所有外掛的設定通過命令列參數進行設定
    • nose2 則通過組態檔進行控制,以最小化命令列參數讓人讀得更舒服

更多對比詳見 官方文件

三、小結

nose 和 nose2 在做到相容 unittest 上就足以看出它們的目標,那便是要吸引原來那些使用 unittest 的使用者來使用它們。它們確實做到了!

nose 和 nose2 在用例編寫、測試夾具、子測試上做出改進,已經能讓日常用例編寫工作變得更加容易和靈活。同時又引入外掛體系,進一步將單元測試框架的能力提升了一個大大的臺階,這讓很多在基礎測試功能之上的高階功能的實現和共用成爲了可能。也難怪有衆多開發者對它們情有獨鍾。