2019年10月31日 星期四

[ Python 文章收集 ] pytest 中的 fixture

Source From Here
Preface
需要被測試內容,如果是沒有副作用的數學函數,給固定輸入就可以得到特定輸出,那麼測試樣例會很容易寫。然而,『沒有副作用』的情況是很少見的,比如 print。所以,在正式進行測試前,往往需要做些準備;而在測試結束後,可能也需要做些清理。本文介紹 pytest 中 setup 與 teardown 的寫法,算是單元測試的進階內容吧。

Setup and teardown
fixture不太好翻譯,大概是『固定裝置』、『測試夾具』這類的意思。如果換一種單元測試常見的稱呼,就比較好理解了——setup 與 teardown,也就是在測試前後,做一些準備和清理。pytest 中 (classic xunit-style setup),支持 setup_* 和 teardown_* 形式的 function 或 method,分別在測試樣例的前後回調。共有 module、function、class 和 method 四種層級(官方文檔中的名詞為 level,也可理解為作用域 scope),大致形式如下:
  1. def setup_module(module):  
  2.     pass  
  3.   
  4. def teardown_module(module):  
  5.     pass  
  6.   
  7. def setup_function(function):  
  8.     pass  
  9.   
  10. def teardown_function(function):  
  11.     pass  
  12.   
  13. class TestSomething:  
  14.     @classmethod  
  15.     def setup_class(cls):  
  16.         pass  
  17.   
  18.     @classmethod  
  19.     def teardown_class(cls):  
  20.         pass  
  21.   
  22.     def setup_method(self, method):  
  23.         pass  
  24.   
  25.     def teardown_method(self, method):  
  26.         pass  
在 pytest 的 3.0 版本以後,上面展示的 module、function、method 參數,可以去掉。當然,class 的 cls 不能去掉。傳參的目的,是支持對這些進行調整,然而大多數情況下是無用的,可以省略。比如,module 的可以寫成:
  1. def setup_module():  
  2.     pass  
  3.   
  4. def teardown_module():  
  5.     pass  
顧名思義,setup_module 就是在同一個 module 的測試執行前回調一次,teardown_module 則是在之後回調。與之相比,setup_function 和 teardown_function 則是在每次 test_* 形式的 function 被執行的前後回調。class 與 method 這一組的機制類似。這種寫法比較古老,是為了兼容 unittest 而保留的,並非 pytest 推薦的寫法。它的問題是,對需要被 setup 和 teardown 的東西,分得不夠細緻。假如,有一個資源——比如一個偽造的數據庫——需要被 3 個測試 function 使用,而這個 module 共有 10 個測試 function。按照這種寫法,就沒有一個簡潔優雅的寫法,令 pytest 僅為這 3 個 function 準備,而不會影響另外 7 個。而 pytest 獨創的 fixture 寫法,就可以完美實現這類場景。

pytest.fixture 用法
底下是一個官方樣例,很明確地展示了 fixture 用法的各個細節:
  1. import pytest  
  2. import smtplib  
  3.   
  4.   
  5. @pytest.fixture(scope="module")  
  6. def smtp():  
  7.     smtp = smtplib.SMTP("smtp.gmail.com"587, timeout=5)  
  8.     yield smtp  
  9.     smtp.close()  
  10.   
  11.   
  12. def test_ehlo(smtp):  
  13.     response, msg = smtp.ehlo()  
  14.     assert response == 250  
  15.     assert b"smtp.gmail.com" in msg  

首先,@pytest.fixture 作為裝飾器(decorator),被它作用的 function 即可成為一個 fixture。scope="module" 是指定作用域。類似 setup_* 與 teardown_*,這裡的 scope 支持 function、class、module、session 四種,默認情況下的 scope 是 function。去掉的 method 由 function 替代,而新增的 session 則是擴大到了整個測試,可以覆蓋多個 module。

fixture 的 function 名稱,可以直接作為參數,傳給需要使用它的測試樣例。在使用時,smtp 並非前面定義的 function,而是 function 的返回值,即 smtplib.SMTP。這一點比較隱晦,稍微違背了 Python 的哲學(詳見《蛇宗三字經》),但卻很方便。yield smtp 當然也可以是 return smtp,不過後面就不能再有語句。相當於只有 setup、沒有 teardown。使用 yield,則後面的內容就是 teardown。這樣不僅方便,把同一組的預備、清理寫在一起,邏輯上也更緊密。最終,在 test_ehlo 中直接聲明一個形式參數 smtp,就可以使用這個 fixture。同一個測試 function 中可以聲明多個這類形式參數,也可以混雜其它類型的參數。如果那些沒有使用 smtp 這個fixture 的 function 被單獨測試,它不會被執行。

另外,在 fixture 中,也可使用其它 fixture 作為形式參數,形成樹狀依賴。這為測試環境的準備,提供了更高的抽象層級

conftest.py: sharing fixture functions
前面有提,fixture 的 scope中,有 session,也就是整個測試過程。這意味著,fixture 可以是全局的,供多個 module 使用。pytest 支持在測試的路徑下,存在 conftest.py 文件,進行全局配置:


在以上目錄結構下,頂層的 conftest.py 裡的配置,可以給四個測試 module 使用。而 sub 下面的 conftest.py,只能給 sub 下面的兩個 module 使用。如果兩個 conftest.py 中定義了名稱相同的 fixture,則可以被覆蓋; 也就是說,在 sub 下面的 module,使用的是 sub 下的 conftest.py 裡的定義同名 fixture。

內置 fixture
以下命令可以列出所有可用的 fixture,包括內置的、插件中的、以及當前項目定義的:
# pytest --fixtures

其中不乏廣泛應用的內容,比如 capsys 和 tmpdir:
  1. capsys  
  2.     Enable capturing of writes to sys.stdout/sys.stderr and make  
  3.     captured output available via ``capsys.readouterr()`` method calls  
  4.     which return a ``(out, err)`` tuple.  ``out`` and ``err`` will be ``text``  
  5.     objects.  
  6. tmpdir  
  7.     Return a temporary directory path object  
  8.     which is unique to each test function invocation,  
  9.     created as a sub directory of the base temporary  
  10.     directory.  The returned object is a `py.path.local`_  
  11.     path object.  
例如:
  1. def test_print(capsys):  
  2.     print('hello')  
  3.     out, err = capsys.readouterr()  
  4.     assert 'hello' == out  
  5.   
  6. def test_path(tmpdir):  
  7.     from py._path.local import LocalPath  
  8.     assert isinstance(tmpdir, LocalPath)  
  9.     from os.path import isdir  
  10.     assert isdir(str(tmpdir))  
capsys 可以捕捉測試 function 的標準輸出,而 tmpdir 則可以自動創建臨時文件夾。它們都是常用 fixture,如果沒有內置,恐怕所有項目都要自行實現。

參數化
有時候,測試一個 function,需要測試多種情況。而每一種情況的測試邏輯基本雷同,只是參數或環境有異。這時就需要參數化(Parametrizing)的 fixture 來減少重複。比如,前面 smtp 那個例子,可能需要準備多個郵箱來測試:
  1. @pytest.fixture(params=["smtp.gmail.com""mail.python.org"])  
  2. def smtp(request):  
  3.     smtp = smtplib.SMTP(request.param, 587, timeout=5)  
  4.     yield smtp  
  5.     print ("finalizing %s" % smtp)  
  6.     smtp.close()  
通過在 @pytest.fixture 中,指定參數 params,就可以利用特殊對象(request)來引用 request.param。使用以上帶參數的 smtp 的測試樣例,都會被執行兩次。還有另一種情況,直接對測試 function 進行參數化:
  1. def add(a, b):  
  2.     return a + b  
  3.   
  4. @pytest.mark.parametrize("test_input, expected", [  
  5.     ([11], 2),  
  6.     ([22], 4),  
  7.     ([01], 1),  
  8. ])  
  9. def test_add(test_input, expected):  
  10.     assert expected == add(test_input[0], test_input[1])  
利用 @pytest.mark.parametrize,可以無需沒有實質意義的 fixture,直接得到參數化的效果,測試多組值。

Summary
學會 fixture 這個利器,pytest 才算真正用到家了。它可以省去很多重複代碼,並且自動管理依賴關係。 當然,pytest 用到家,不代表 Python 測試就可以畢業了。畢竟,有些環境是無法準備的,有些開銷是可以避免的。

Supplement
pytest 的插件介紹:pytest-cov、pytest-pep8 與 pytest-flakes
pytest 中使用 mock
Python 項目的 pytest 初始化
用 pytest-httpserver 來測試 requests
配置 yapf 和 isort 的 Vim 與 Pytest 插件

沒有留言:

張貼留言

[Git 常見問題] error: The following untracked working tree files would be overwritten by merge

  Source From  Here 方案1: // x -----删除忽略文件已经对 git 来说不识别的文件 // d -----删除未被添加到 git 的路径中的文件 // f -----强制运行 #   git clean -d -fx 方案2: 今天在服务器上  gi...