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 插件

2019年10月29日 星期二

[ Python 常見問題 ] How to enter PDB on a specific stack frame

Source From Here
Question
I've written a function to enter PDB when an exception is raised (let's call it trace_on_error). Right now when I call pdb.set_trace()pdb reasonably enters into the stack frame of trace_on_error, requiring me to have to type the up command before being able to look at the frame of the calling function.

I am trying to make trace_on_error not require users to know how its implemented to use, and thus i would like to have pdb enter into the callers stack frame. As I looked for documentation, i was hoping to find something similar to pdb.set_trace(frame_up=1), but I have not found anything.

Example Code:
  1. def trace_on_error(f, errors):  
  2.     try:  
  3.         return f()  
  4.     except errors as e:  
  5.         pdb.set_trace()  
How-To
This should do the trick (I've tested it but not sure it works in every possible case):
  1. def trace_on_error(f, errors):  
  2.     try:  
  3.         return f()  
  4.     except errors as e:  
  5.         import sys  
  6.         from pdb import Pdb  
  7.         Pdb().set_trace(sys._getframe().f_back)  
For sys._getframe([depth])
Return a frame object from the call stack. If optional integer depth is given, return the frame object that many calls below the top of the stack. If that is deeper than the call stack, ValueError is raised. The default for depth is zero, returning the frame at the top of the call stack.

Supplement
Python standard library: inspect — Inspect live objects

[ Python 文章收集 ] Monitoring memory usage of a running Python program

Source From  Here Preface At  Survata , we do a lot of data processing using Python and its suite of data processing libraries like  pandas ...