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),大致形式如下:
- def setup_module(module):
- pass
- def teardown_module(module):
- pass
- def setup_function(function):
- pass
- def teardown_function(function):
- pass
- class TestSomething:
- @classmethod
- def setup_class(cls):
- pass
- @classmethod
- def teardown_class(cls):
- pass
- def setup_method(self, method):
- pass
- def teardown_method(self, method):
- pass
- def setup_module():
- pass
- def teardown_module():
- pass
pytest.fixture 用法
底下是一個官方樣例,很明確地展示了 fixture 用法的各個細節:
- import pytest
- import smtplib
- @pytest.fixture(scope="module")
- def smtp():
- smtp = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
- yield smtp
- smtp.close()
- def test_ehlo(smtp):
- response, msg = smtp.ehlo()
- assert response == 250
- assert b"smtp.gmail.com" in msg
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,包括內置的、插件中的、以及當前項目定義的:
其中不乏廣泛應用的內容,比如 capsys 和 tmpdir:
- capsys
- Enable capturing of writes to sys.stdout/sys.stderr and make
- captured output available via ``capsys.readouterr()`` method calls
- which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text``
- objects.
- tmpdir
- Return a temporary directory path object
- which is unique to each test function invocation,
- created as a sub directory of the base temporary
- directory. The returned object is a `py.path.local`_
- path object.
- def test_print(capsys):
- print('hello')
- out, err = capsys.readouterr()
- assert 'hello' == out
- def test_path(tmpdir):
- from py._path.local import LocalPath
- assert isinstance(tmpdir, LocalPath)
- from os.path import isdir
- assert isdir(str(tmpdir))
參數化
有時候,測試一個 function,需要測試多種情況。而每一種情況的測試邏輯基本雷同,只是參數或環境有異。這時就需要參數化(Parametrizing)的 fixture 來減少重複。比如,前面 smtp 那個例子,可能需要準備多個郵箱來測試:
- @pytest.fixture(params=["smtp.gmail.com", "mail.python.org"])
- def smtp(request):
- smtp = smtplib.SMTP(request.param, 587, timeout=5)
- yield smtp
- print ("finalizing %s" % smtp)
- smtp.close()
- def add(a, b):
- return a + b
- @pytest.mark.parametrize("test_input, expected", [
- ([1, 1], 2),
- ([2, 2], 4),
- ([0, 1], 1),
- ])
- def test_add(test_input, expected):
- assert expected == add(test_input[0], test_input[1])
Summary
學會 fixture 這個利器,pytest 才算真正用到家了。它可以省去很多重複代碼,並且自動管理依賴關係。 當然,pytest 用到家,不代表 Python 測試就可以畢業了。畢竟,有些環境是無法準備的,有些開銷是可以避免的。
Supplement
* pytest 的插件介紹:pytest-cov、pytest-pep8 與 pytest-flakes
* pytest 中使用 mock
* Python 項目的 pytest 初始化
* 用 pytest-httpserver 來測試 requests
* 配置 yapf 和 isort 的 Vim 與 Pytest 插件