1. 程式人生 > >(!)Python 各種測試框架簡介

(!)Python 各種測試框架簡介

一、doctest

doctest 是一個 Python 發行版自帶的標準模組。本篇將分別對使用 doctest 的兩種方式——嵌入到原始碼中和做成獨立檔案做基本介紹。

1.doctest的概念模型

在 Python 的官方文件中,對 doctest 的介紹是這樣的:

doctest 模組會搜尋那些看起來像互動式會話的 Python 程式碼片段,然後嘗試執行並驗證結果

即使從沒接觸過 doctest,我們也可以從這個名字中窺到一絲端倪。“它看起來就像程式碼裡的文件字串(docstring)一樣” 如果你這麼想的話,就已經對了一半了。

doctest 的編寫過程就彷彿你真的在一個互動式 shell(比如 idle)中匯入了要測試的模組,然後開始一條條地測試模組裡的函式一樣。實際上有很多人也是這麼做的,他們寫好一個模組後,就在 shell 裡挨個測試函式,最後把 shell 會話複製貼上成 doctest 用例。

2.嵌入原始碼模式

下面使用的例子是一個只有一個函式的模組,其中籤入了兩個 doctest 的測試用例。

unnecessary_math.py:


"""
這裡也可以寫
"""
def multiply(a,b):
    """
    >>> multiply(2,3)
    6
    >>> multiply('baka~',3)
    'baka~baka~baka~'
    """
    return a*b

if __name__ == '__main__':
    import doctest
    doctest.testmod(verbose=True
)

注意測試程式碼的位置,前面說過 doctest 的測試用例就像文件字串一樣,這句話的內涵在於:測試用例的位置必須放在整個模組檔案的開頭,或者緊接著物件宣告語句的下一行。也就是可以被 _ doc _ 這個屬性引用到的地方。並非像普通註釋一樣寫在哪裡都可以。另:verbose 引數用於控制是否輸出詳細資訊,預設為 False,如果不寫,那麼執行時不會輸出任何東西,除非測試 fail。

示例的執行輸出為:

Trying:
multiply(2,3)
Expecting:
6
ok
Trying:
multiply(‘baka~’,3)
Expecting:
‘baka~baka~baka~’
ok
1 items had no tests:
main


1 items passed all tests:
2 tests in main.multiply
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

值得一提的是,如果將這個指令碼儲存為doctest.py,並且執行,你會得到以下結果:

Traceback (most recent call last):
File “doctest.py”, line 62, in
doctest.testmod()
AttributeError: ‘module’ object has no attribute ‘testmod’

原因是被重寫了,把檔名改成doctest1.py(或其他名字)之後,需要刪除之前的pyc檔案。再執行即可。

上例中啟動測試的方式是在 _ main _ 函式裡呼叫了 doctest.testmod() 函式。這對於純容器型模組檔案來說是一個好辦法——正常使用時只做匯入用,直接執行檔案則進行測試。而對於 _ main _ 函式另有他用的情況,則還可以通過命令列來啟動測試:

$ python -m doctest unnecessary_math.py
$ python -m doctest -v unnecessary_math.py

這裡-m 表示引用一個模組,-v 等價於 verbose=True。執行輸出與上面基本一樣。

3.獨立檔案模式

如果不想(或不能)把測試用例寫進原始碼裡,則還可以使用一個獨立的文字檔案來儲存測試用例。

可選的一些解釋性內容...

>>> from test import multiply
>>> multiply(2,3)
6
>>> multiply('baka~',3)
'baka~baka~baka~'

幾乎同樣的格式。執行方法可以分為在 Python shell 裡執行或者在系統 shell 裡執行:

>>> import doctest
>>> doctest.testfile('example.txt')

bash/cmd.exe:

$ python -m doctest -v example.txt

【摘自:連結1

二、unittest

unittest 與 doctest 一樣也是 Python 發行版自帶的包。如果你聽說過 PyUnit(OSC 開源專案頁面中就有 PyUnit 的頁面),那麼這倆其實是同一個東西——PyUnit 是 unittest 的曾用名,因為 PyUnit 最早也是來源於 Kent 和 Erich 的 JUnit(xUnit 測試框架系列的 Java 版本)

1.unittest 概覽

上一篇介紹的 doctest 不管是看起來還是用起來都顯得十分簡單,可以與原始碼寫在一起,比較適合用作驗證性的功能測試。而本篇的 unittest 從名字上看,它是一個單元測試框架;從官方文件的字數上看,它的能力應該比 doctest 強一些。

使用 unittest 的標準流程為:

1. 從 unittest.TestCase 派生一個子類
2. 在類中定義各種以 “test_” 打頭的方法
3. 通過 unittest.main() 函式來啟動測試

unittest 的一個很有用的特性是 TestCase 的 setUp()tearDown() 方法,它們提供了為測試進行準備和掃尾工作的功能,聽起來就像上下文管理器一樣。這種功能很適合用在測試物件需要複雜執行環境的情況下。

2.舉個例子

這裡依舊使用上篇中那個極簡的例子:unnecessary_math.py 檔案中有一個 multiply() 函式,功能與 * 操作符完全一樣。

test_um_test.py

import unittest
from unnecessary_math import multiply

class TestUM(unittest.TestCase):
    def setUp(self):
        pass
    def test_number_3_4(self):
        self.assertEqual(multiply(3,4),12)
    def test_string_a_3(self):
        self.assertEqual(multiply('a',3),'aaa')

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

這個例子裡,我們使用了 assertEqual() 方法。unittest 中還有很多類似的 assert 方法,比如 NotEqualIs(Not)NoneTrue(False)Is(Not)Instance 等針對變數值的校驗方法;另外還有一些如 assertRaises()assertRaisesRegex() 等針對異常、警告和 log 的檢查方法;以及如 assertAlmostEqual() 等一些奇怪的方法。

3.啟動測試

上例中的結尾處,我們定義了一個對 unittest.main() 的呼叫,因此這個指令碼是可以直接執行的:

$ python test_um_test.py
..
--------------------------------------
Ran 2 tests in 0.01s

OK

同樣 -v 引數是可選的,也可以在 unittest.main() 函式裡直接指定:verbosity=1

4.Test Discovery

這個分段標題我暫時沒想到好的翻譯方法,就先不翻了。

Test Discovery 的作用是:假設你的專案資料夾裡面四散分佈著很多個測試檔案。當你做迴歸測試的時候,一個一個地執行這些測試檔案就太麻煩了。TestLoader.discover() 提供了一個可以在專案目錄下自動搜尋並執行測試檔案的功能,並可以直接從命令列呼叫:

$ cd project_directory
$ python -m unittest discover

discover 可用的引數有 4 個(-v -s -p -t),其中 -s-t 都與路徑有關,如上例中提前 cd 到專案路徑的話這倆引數都可以無視;-v 喜聞樂見;-p 是 –pattern 的縮寫,可用於匹配某一類檔名。

5.測試環境

當類裡面定義了 setUp() 方法的時候,測試程式會在執行每條測試項前先呼叫此方法;同樣地,在全部測試項執行完畢後,tearDown() 方法也會被呼叫。驗證如下:

import unittest

class simple_test(unittest.TestCase):
    def setUp(self):
        self.foo = list(range(10))

    def test_1st(self):
        self.assertEqual(self.foo.pop(),9)

    def test_2nd(self):
        self.assertEqual(self.foo.pop(),9)

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

注意這裡兩次測試均對同一個例項屬性 self.foo 進行了 pop() 呼叫,但測試結果均為 pass,即說明,test_1st 和 test_2nd 在呼叫前都分別呼叫了一次 setUp()

那如果我們想全程只調用一次 setUp/tearDown 該怎麼辦呢?就是用 setUpClass()tearDownClass() 類方法啦。注意使用這兩個方法的時候一定要用 @classmethod 裝飾器裝飾起來:

import unittest

class simple_test(unittest.TestCase):
    @classmethod
    def setUpClass(self):
        self.foo = list(range(10))

    def test_1st(self):
        self.assertEqual(self.foo.pop(),9)

    def test_2nd(self):
        self.assertEqual(self.foo.pop(),8)

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

這個例子裡我們使用了一個類級別的 setUpClass() 類方法,並修改了第二次 pop() 呼叫的預期返回值。執行結果顯示依然是全部通過,即說明這次在全部測試項被呼叫前只調用了一次 setUpClass()

再往上一級,我們希望在整個檔案級別上只調用一次 setUp/tearDown,這時候就要用 setUpModule() 和 tearDownModule() 這兩個函數了,注意是函式,與 TestCase 類同級:

import unittest

def setUpModule():
    pass

class simple_test(inittest.TestCase):
    ...

一般 assert*() 方法如果丟擲了未被捕獲的異常,那麼這條測試用例會被記為 fail,測試繼續進行。但如果異常發生在 setUp() 裡,就會認為測試程式自身存在錯誤,後面的測試用例和 tearDown() 都不會再執行。即,tearDown() 僅在 setUp() 成功執行的情況下才會執行,並一定會被執行

最後,這兩個方法的預設實現都是什麼都不做(只有一句 pass),所以覆蓋的時候直接寫新內容就可以了,不必再呼叫父類的此方法。

三、nose

本篇將介紹的 nose 不再是 Python 官方發行版的標準包,但它與 unittest 有著千絲萬縷的聯絡。比如 nose 的口號就是:

擴充套件 unittest,nose 讓測試更簡單。

1.簡單在哪

自古(1970)以來,任何標榜“更簡單”的工具所使用的手段基本都是隱藏細節,nose 也不例外。nose 不使用特定的格式、不需要一個類容器,甚至不需要 import nose ~(這也就意味著它在寫測試用例時不需要使用額外的 api)

前兩篇中一直使用的 unnecessary_math.py 的 nose 版測試用例是這樣子的:

from unnecessary_math import multiply

def test_numbers():
    assert multiply(3,4)==12

def test_strings():
    assert multiply('a',3)=='aaa'

看上去完全就是一個普通的模組檔案嘛,甚至連 main 函式都不用。這裡唯一需要一點“講究”的語法在於:測試用例的命名仍需以 test_ 開頭

2.執行 nose

nose 在安裝的時候也向你 Python 根目錄下的 Scripts 資料夾內添加了一個名為 nosetests 的可執行檔案,這個可執行檔案就是用來執行測試的命令;當然你也仍可以使用 -m 引數來呼叫 nose 模組:

$ nosetests test.py
$ python -m nose test.py
··
------------------------------------------------
Ran 2 tests in 0.001s

OK

另外非常棒的一點是,nosetests 相容對 doctest 和 unittest 測試指令碼的解析執行。如果你認為 nose 比那兩個都好用的話,完全可以放棄 doctest 和 unittest 的使用。

3.測試環境

由於擴充套件自 unittest,nose 也支援類似於 setUp() setUpClass() setUpModule() 的測試環境建立方式,只不過函式命名規則最好改一改,我們可以使用更符合 Python 規範的命名規則。另外因為 nose 支援上例中所展示的函式式測試用例,所以還有一種為單個函式建立執行環境的裝飾器可用。下面我們將使用一個例子來展示這四種功能的用法。

test.py:

from nose import with_setup 
from unnecessary_math import multiply

def setup_module(module):
    print('setup_module 函式執行於一切開始之前')

def setup_deco():
    print('setup_deco 將用於 with_setup')

def teardown_deco():
    print('teardown_deco 也將用於 with_setup')

@with_setup(setup_deco,teardown_deco)
def test_2b_decorated():
    assert multiply(3,4)==12

class TestUM():
    def setup(self):
        print('setup 方法執行於本類中每條用例之前')

    @classmethod
    def setup_class(cls):
        print('setup_class 類方法執行於本類中任何用例開始之前,且僅執行一次')

    def test_strings(self):
        assert multiply('a',3)=='aaa'

執行 $ nosetests -v test.py 結果如下:

test.TestUM.test_strings … ok
test.test_2b_decorated … ok

Ran 2 tests in 0.002s

OK

我們的 print() 函式一點東西都沒打出來,如果你想看的話,給 nosetests 新增一個 -s 引數就可以了。

4.Test Discovery

nose 的 discovery 規則為:

1.長得像測試用例,那就是測試用例。路徑、模組(檔案)、類、函式的名字如果能和 testMatch 正則表示式匹配上,那就會被認為是一個用例。另外所有 unittest.TestCase 的子類也都會被當做測試用例。(這裡的 testMatch 可能是個環境變數之類的東西,我沒有去查,因為反正你只要以 test_ 開頭的格式來命名就可以保證能被發現)

2.如果一個資料夾既長得不像測試用例,又不是一個包(路徑下沒有 init.py)的話,那麼 nose 就會略過對這個路徑的檢查。

3.但只要一個資料夾是一個包,那麼 nose 就一定會去檢查這個路徑。

4.顯式避免某個物件被當做測試用例的方法為:給其或其容器新增一個 _ test _ 屬性,並且運算結果不為 True。並不需要直接指定為 False,只要 bool( _ test _ ) == False 即可。另外,這個屬性的新增方式比較特別,確認自己已經掌握使用方法前最好都試試。例如在類裡面需要新增為類屬性而非例項屬性(即不能寫在 _ init _(self) 裡),否則不起作用。這裡因為只是簡介,就不挨個試了。(官方文件裡就沒解釋清楚…)

呼叫 discovery 的語法為,cd 到目錄後直接呼叫 $ nosetests,後面不跟具體的檔名。另外這種方法其實對 unittest 也適用。

四、pytest

pytest 有時也被稱為 py.test,是因為它使用的執行命令是 $ py.test。本文中我們使用 pytest 指代這個測試框架,py.test 特指執行命令。

1.較於 nose

這裡沒有使用像前三篇一樣(簡介-舉例-discovery-環境)式的分段展開,是因為 pytest 與 nose 的基本用法極其相似。因此只做一個比較就好了。他倆的區別僅在於

1.呼叫測試的命令不同,pytest 用的是 $ py.test
2.建立測試環境(setup/teardown)的 api 不同

下面使用一個例子說明 pytest 的 setup/teardown 使用方式。

some_test.py:

import pytest

@pytest.fixture(scope='function')
def setup_function(request):
    def teardown_function():
        print("teardown_function called.")
    request.addfinalizer(teardown_function)
    print('setup_function called.')

@pytest.fixture(scope='module')
def setup_module(request):
    def teardown_module():
        print("teardown_module called.")
    request.addfinalizer(teardown_module)
    print('setup_module called.')


def test_1(setup_function):
    print('Test_1 called.')

def test_2(setup_module):
    print('Test_2 called.')

def test_3(setup_module):
    print('Test_3 called.')

pytest 建立測試環境(fixture)的方式如上例所示,通過顯式指定 scope=” 引數來選擇需要使用的 pytest.fixture 裝飾器。即一個 fixture 函式的型別從你定義它的時候就確定了,這與使用 @nose.with_setup() 十分不同。對於 scope=’function’ 的 fixture 函式,它就是會在測試用例的前後分別呼叫 setup/teardown。測試用例的引數如 def test_1(setup_function) 只負責引用具體的物件,它並不關心對方的作用域是函式級的還是模組級的。

有效的 scope 引數限於:’function’,’module’,’class’,’session’,預設為 function。

執行上例:$ py.test some_test.py -s。 -s 用於顯示 print() 函式

============================= test session starts =============================
platform win32 -- Python 3.3.2 -- py-1.4.20 -- pytest-2.5.2
collected 3 items

test.py setup_function called.
Test_1 called.
.teardown_function called.
setup_module called.
Test_2 called.
.Test_3 called.
.teardown_module called.


========================== 3 passed in 0.02 seconds ===========================

這裡需要注意的地方是:setup_module 被呼叫的位置。

2.pytest 與 nose 二選一

首先,單是從不需要使用特定類模板的角度上,nose 和 pytest 就較於 unittest 好出太多了。doctest 比較奇葩我們在這裡不比。因此對於 “選一個自己喜歡的測試框架來用” 的問題,就變成了 nose 和 pytest 二選一的問題。

pythontesting.net 的作者非常喜歡 pytest,並表示

“如果你挑不出 pytest 的毛病,就用這個吧”。

於是下面我們就來挑挑 pytest 的毛病:

它的 setup/teardown 語法與 unittest 的相容性不如 nose 高,實現方式也不如 nose 直觀
第一條足矣
畢竟 unittest 還是 Python 自帶的單元測試框架,肯定有很多怕麻煩的人在用,所以與其語法保持一定相容效能避免很多麻煩。即使 pytest 在命令列中有彩色輸出讓我很喜歡,但這還是不如第一條重要。

實際上,PyPI 中 nose 的下載量也是 pytest 的 8 倍多。

所以假如再繼續寫某一個框架的詳解的話,大概我會選 nose 吧。

[摘自:連結2]