聊聊 Python 的單元測試框架(二):nose 和它的繼任者 nose2
- 2019 年 10 月 8 日
- 筆記
作者:HelloGitHub-Prodesire
出處:HelloGitHub
文中涉及的示例程式碼,已同步更新到 HelloGitHub-Team 倉庫 點擊本文最下方的「閱讀原文」即可獲取
一、nose
nose[1] 是一個第三方單元測試框架,它完全兼容 unittest
,並且號稱是一個更好用的測試框架。
那麼 nose
除了具備 unittest
的所有功能外,還具有哪些優勢呢?
1.1 用例編寫
用例的編寫方式除了編寫繼承於 unittest.TestCase[2] 的測試類外,還可以編寫成沒有繼承的測試類。比如,寫成如下形式也會被 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[3] 裝飾器裝飾測試函數,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[4]:在所有模組中收集用例
- Attrib[5]:給用例打標籤,並可運行含指定標籤的用例
- Capture[6]:捕獲用例的標準輸出
- Collect[7]:快速收集用例
- Cover[8]:統計程式碼覆蓋率
- Debug[9]:用例失敗時進入 pdb 調試
- Deprecated[10]:標記用例為棄用
- Doctests[11]:運行文檔用例
- Failure Detail[12]:斷言失敗時提供上下文資訊
- Isolate[13]:保護用例避免受一些副作用的影響
- Logcapture[14]:捕捉 logging 輸出
- Multiprocess[15]:並行執行用例
- Prof[16]:使用熱點分析器進行分析
- Skip[17]:標記用例為跳過
- Testid[18]:為輸出的每個用例名稱添加測試 ID
- Xunit[19]:以 xunit 格式輸出測試結果
而第三方庫則多種多樣,如用來生成 HTML 格式測試報告的 nose-htmloutput[20] 等,這裡不再一一列出。
得益於 nose
豐富的插件生態,當 nose
本身不能夠完全滿足我們的測試需求時,可以通過安裝插件,並在 nosetests
命令行指定該插件所提供的特定參數即可非常容易的使用插件。相較於 unittest
,就能省去很多自己開發額外測試邏輯的精力。
二、nose2
nose2[21] 是 nose[22] 的繼任者。它們的理念都是讓編寫和運行測試用例變得更容易。
它們有很多相同點,比如都兼容 unittest
,支援使用函數作為測試用例,支援子測試,擁有插件體系。但也有很多不同點,下面列出一些主要的不同點:
- 發現和載入測試
nose
自行實現了模組載入功能,使用惰性方式載入測試模組,載入一個執行一個。nose2
則藉助內建的 **import**()[23] 導入模組,並且是先全部載入,再執行用例nose2
並不支援nose
所支援的所有測試用例項目結構,比如如下用例文件的結構在nose2
中就不受支援:
. `-- tests |-- more_tests | `-- test.py `-- test.py
- 測試前置和清理函數級別
nose
支援方法、類、模組和包級別的測試前置和清理函數nose2
則不支援包級別的測試前置和清理函數
- 子測試
nose2
除了支援使用測試生成器來實現子測試外,還支援使用參數化測試(Parameterized tests)[24]來實現子測試nose2
除了像nose
一樣支援在測試函數和測試類(不繼承於unittest.TestCase
)中支援參數化測試和測試生成器外,還支援在繼承於unittest.TestCase
的測試類中使用
- 配置化
nose
期望所有插件的配置通過命令行參數進行配置nose2
則通過配置文件進行控制,以最小化命令行參數讓人讀得更舒服
更多對比詳見 官方文檔[25]。
三、小結
nose
和 nose2
在做到兼容 unittest
上就足以看出它們的目標,那便是要吸引原來那些使用 unittest
的用戶來使用它們。它們確實做到了!
nose
和 nose2
在用例編寫、測試夾具、子測試上做出改進,已經能讓日常用例編寫工作變得更加容易和靈活。同時又引入插件體系,進一步將單元測試框架的能力提升了一個大大的台階,這讓很多在基礎測試功能之上的高階功能的實現和共享成為了可能。也難怪有眾多開發者對它們情有獨鍾。