使用自訂標記¶
以下是一些使用 如何使用屬性標記測試函式 機制的範例。
標記測試函式並選取它們來執行¶
您可以使用自訂的元資料「標記」測試函式,如下所示
# content of test_server.py
import pytest
@pytest.mark.webtest
def test_send_http():
pass # perform some webtest test for your app
def test_something_quick():
pass
def test_another():
pass
class TestClass:
def test_method(self):
pass
然後,您可以限制測試執行,只執行標記為 webtest
的測試
$ pytest -v -m webtest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 4 items / 3 deselected / 1 selected
test_server.py::test_send_http PASSED [100%]
===================== 1 passed, 3 deselected in 0.12s ======================
或者相反,執行所有測試,除了 webtest 測試
$ pytest -v -m "not webtest"
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 4 items / 1 deselected / 3 selected
test_server.py::test_something_quick PASSED [ 33%]
test_server.py::test_another PASSED [ 66%]
test_server.py::TestClass::test_method PASSED [100%]
===================== 3 passed, 1 deselected in 0.12s ======================
根據節點 ID 選取測試¶
您可以提供一個或多個 節點 ID 作為位置參數,以只選取指定的測試。這讓您可以輕鬆地根據模組、類別、方法或函式名稱選取測試
$ pytest -v test_server.py::TestClass::test_method
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 1 item
test_server.py::TestClass::test_method PASSED [100%]
============================ 1 passed in 0.12s =============================
您也可以根據類別選取
$ pytest -v test_server.py::TestClass
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 1 item
test_server.py::TestClass::test_method PASSED [100%]
============================ 1 passed in 0.12s =============================
或者選取多個節點
$ pytest -v test_server.py::TestClass test_server.py::test_send_http
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 2 items
test_server.py::TestClass::test_method PASSED [ 50%]
test_server.py::test_send_http PASSED [100%]
============================ 2 passed in 0.12s =============================
注意
節點 ID 的格式為 module.py::class::method
或 module.py::function
。節點 ID 控制收集哪些測試,因此 module.py::class
會選取類別上的所有測試方法。也會為參數化固定裝置或測試的每個參數建立節點,因此選取參數化測試時必須包含參數值,例如 module.py::function[param]
。
當使用 -rf
選項執行 pytest 時,失敗測試的節點 ID 會顯示在測試摘要資訊中。您也可以從 pytest --collect-only
的輸出建立節點 ID。
使用 -k expr
根據名稱選取測試¶
新增於版本 2.0/2.3.4。
您可以使用 -k
命令列選項指定一個表達式,在測試名稱上實作子字串比對,而不是 -m
提供的標記完全比對。這讓您可以輕鬆地根據名稱選取測試
已於版本 5.4 中變更。
表達式比對現在不區分大小寫。
$ pytest -v -k http # running with the above defined example module
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 4 items / 3 deselected / 1 selected
test_server.py::test_send_http PASSED [100%]
===================== 1 passed, 3 deselected in 0.12s ======================
您也可以執行所有測試,除了與關鍵字比對的測試。
$ pytest -k "not send_http" -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 4 items / 1 deselected / 3 selected
test_server.py::test_something_quick PASSED [ 33%]
test_server.py::test_another PASSED [ 66%]
test_server.py::TestClass::test_method PASSED [100%]
===================== 3 passed, 1 deselected in 0.12s ======================
或選擇「http」和「快速」測試
$ pytest -k "http or quick" -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 4 items / 2 deselected / 2 selected
test_server.py::test_send_http PASSED [ 50%]
test_server.py::test_something_quick PASSED [100%]
===================== 2 passed, 2 deselected in 0.12s ======================
您可以使用 and
、or
、not
和括號。
除了測試名稱,-k
也會比對測試父項目的名稱(通常是檔案和類別名稱),設定在測試函式的屬性、套用在測試或其父項目的標記,以及任何 extra keywords
明確新增到測試或其父項目。
註冊標記¶
為測試套件註冊標記很簡單
# content of pytest.ini
[pytest]
markers =
webtest: mark a test as a webtest.
slow: mark test as slow.
可以註冊多個自訂標記,方法是在每一個標記的專屬列中定義,如上方的範例所示。
您可以詢問測試套件有哪些標記 - 清單中包含我們剛剛定義的 webtest
和 slow
標記
$ pytest --markers
@pytest.mark.webtest: mark a test as a webtest.
@pytest.mark.slow: mark test as slow.
@pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://pytest.dev.org.tw/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings
@pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.
@pytest.mark.skipif(condition, ..., *, reason=...): skip the given test function if any of the conditions evaluate to True. Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. See https://pytest.dev.org.tw/en/stable/reference/reference.html#pytest-mark-skipif
@pytest.mark.xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict): mark the test function as an expected failure if any of the conditions evaluate to True. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://pytest.dev.org.tw/en/stable/reference/reference.html#pytest-mark-xfail
@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://pytest.dev.org.tw/en/stable/how-to/parametrize.html for more info and examples.
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://pytest.dev.org.tw/en/stable/explanation/fixtures.html#usefixtures
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. DEPRECATED, use @pytest.hookimpl(trylast=True) instead.
如需如何從外掛程式新增和使用標記的範例,請參閱 自訂標記和命令列選項,用於控制測試執行。
注意
建議明確註冊標記,以便
在測試套件中有一個地方定義標記
透過
pytest --markers
詢問現有標記會產生良好的輸出如果您使用
--strict-markers
選項,函式標記中的錯字會視為錯誤。
標記整個類別或模組¶
您可以將 pytest.mark
裝飾器與類別搭配使用,將標記套用至所有測試方法
# content of test_mark_classlevel.py
import pytest
@pytest.mark.webtest
class TestClass:
def test_startup(self):
pass
def test_startup_and_more(self):
pass
這等同於直接將裝飾器套用至兩個測試函式。
要套用模組層級的標記,請使用 pytestmark
全域變數
import pytest
pytestmark = pytest.mark.webtest
或多個標記
pytestmark = [pytest.mark.webtest, pytest.mark.slowtest]
基於舊有原因,在類別裝飾器被引入之前,可以像這樣在測試類別上設定 pytestmark
屬性
import pytest
class TestClass:
pytestmark = pytest.mark.webtest
使用 parametrize 時標記個別測試¶
使用 parametrize 時,套用標記會讓它套用至個別測試。然而,也可以套用標記至個別測試實例
import pytest
@pytest.mark.foo
@pytest.mark.parametrize(
("n", "expected"), [(1, 2), pytest.param(1, 3, marks=pytest.mark.bar), (2, 3)]
)
def test_increment(n, expected):
assert n + 1 == expected
在此範例中,標記「foo」會套用至三個測試中的每個測試,而標記「bar」僅套用至第二個測試。也可以用這種方式套用跳過和失敗標記,請參閱 使用 parametrize 跳過/失敗。
自訂標記和命令列選項用於控制測試執行¶
外掛程式可以提供自訂標記,並根據它實作特定行為。這是一個獨立的範例,它新增一個命令列選項和一個參數化測試函式標記,用於執行透過命名環境指定的測試
# content of conftest.py
import pytest
def pytest_addoption(parser):
parser.addoption(
"-E",
action="store",
metavar="NAME",
help="only run tests matching the environment NAME.",
)
def pytest_configure(config):
# register an additional marker
config.addinivalue_line(
"markers", "env(name): mark test to run only on named environment"
)
def pytest_runtest_setup(item):
envnames = [mark.args[0] for mark in item.iter_markers(name="env")]
if envnames:
if item.config.getoption("-E") not in envnames:
pytest.skip(f"test requires env in {envnames!r}")
使用此本機外掛程式的測試檔案
# content of test_someenv.py
import pytest
@pytest.mark.env("stage1")
def test_basic_db_operation():
pass
以及指定不同於測試所需環境的範例呼叫
$ pytest -E stage2
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item
test_someenv.py s [100%]
============================ 1 skipped in 0.12s ============================
而這裡指定了所需環境
$ pytest -E stage1
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item
test_someenv.py . [100%]
============================ 1 passed in 0.12s =============================
--markers
選項會提供可用標記的清單
$ pytest --markers
@pytest.mark.env(name): mark test to run only on named environment
@pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://pytest.dev.org.tw/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings
@pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.
@pytest.mark.skipif(condition, ..., *, reason=...): skip the given test function if any of the conditions evaluate to True. Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. See https://pytest.dev.org.tw/en/stable/reference/reference.html#pytest-mark-skipif
@pytest.mark.xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict): mark the test function as an expected failure if any of the conditions evaluate to True. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://pytest.dev.org.tw/en/stable/reference/reference.html#pytest-mark-xfail
@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://pytest.dev.org.tw/en/stable/how-to/parametrize.html for more info and examples.
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://pytest.dev.org.tw/en/stable/explanation/fixtures.html#usefixtures
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. DEPRECATED, use @pytest.hookimpl(trylast=True) instead.
傳遞可呼叫項給自訂標記¶
以下是會用於下一個範例的組態檔
# content of conftest.py
import sys
def pytest_runtest_setup(item):
for marker in item.iter_markers(name="my_marker"):
print(marker)
sys.stdout.flush()
自訂標記可以設定其引數,也就是 args
和 kwargs
屬性,定義方式是呼叫它為可呼叫項或使用 pytest.mark.MARKER_NAME.with_args
。這兩種方法在大部分時間都達到相同的效果。
然而,如果有一個可呼叫的單一位置參數沒有關鍵字參數,使用 pytest.mark.MARKER_NAME(c)
將不會傳遞 c
作為位置參數,而是使用自訂標記裝飾 c
(請參閱 MarkDecorator)。幸運的是,pytest.mark.MARKER_NAME.with_args
可以派上用場
# content of test_custom_marker.py
import pytest
def hello_world(*args, **kwargs):
return "Hello World"
@pytest.mark.my_marker.with_args(hello_world)
def test_with_args():
pass
輸出如下
$ pytest -q -s
Mark(name='my_marker', args=(<function hello_world at 0xdeadbeef0001>,), kwargs={})
.
1 passed in 0.12s
我們可以看到自訂標記已設定其參數,並使用函數 hello_world
進行延伸。這是將自訂標記建立為可呼叫(在幕後呼叫 __call__
)與使用 with_args
之間的主要差異。
讀取從多個位置設定的標記¶
如果您在測試套件中大量使用標記,您可能會遇到標記被套用於測試函數多次的情況。從外掛程式碼中,您可以讀取所有此類設定。範例
# content of test_mark_three_times.py
import pytest
pytestmark = pytest.mark.glob("module", x=1)
@pytest.mark.glob("class", x=2)
class TestClass:
@pytest.mark.glob("function", x=3)
def test_something(self):
pass
在這裡,我們將標記 “glob” 套用於同一個測試函數三次。從 conftest 檔案中,我們可以這樣讀取
# content of conftest.py
import sys
def pytest_runtest_setup(item):
for mark in item.iter_markers(name="glob"):
print(f"glob args={mark.args} kwargs={mark.kwargs}")
sys.stdout.flush()
讓我們在不擷取輸出的情況下執行此操作,看看我們得到什麼
$ pytest -q -s
glob args=('function',) kwargs={'x': 3}
glob args=('class',) kwargs={'x': 2}
glob args=('module',) kwargs={'x': 1}
.
1 passed in 0.12s
使用 pytest 標記特定平台測試¶
假設您有一個測試套件,它標記了特定平台的測試,即 pytest.mark.darwin
、pytest.mark.win32
等,並且您還有在所有平台上執行且沒有特定標記的測試。如果您現在想要一種方法來僅執行特定平台的測試,您可以使用以下外掛程式
# content of conftest.py
#
import sys
import pytest
ALL = set("darwin linux win32".split())
def pytest_runtest_setup(item):
supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers())
plat = sys.platform
if supported_platforms and plat not in supported_platforms:
pytest.skip(f"cannot run on platform {plat}")
然後,如果測試被指定為不同平台,則會略過測試。讓我們做一個小測試檔案來展示它的樣子
# content of test_plat.py
import pytest
@pytest.mark.darwin
def test_if_apple_is_evil():
pass
@pytest.mark.linux
def test_if_linux_works():
pass
@pytest.mark.win32
def test_if_win32_crashes():
pass
def test_runs_everywhere():
pass
然後,您將看到兩個測試被略過,兩個測試被執行,正如預期的那樣
$ pytest -rs # this option reports skip reasons
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 4 items
test_plat.py s.s. [100%]
========================= short test summary info ==========================
SKIPPED [2] conftest.py:13: cannot run on platform linux
======================= 2 passed, 2 skipped in 0.12s =======================
請注意,如果您通過標記命令列選項指定平台,如下所示
$ pytest -m linux
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 4 items / 3 deselected / 1 selected
test_plat.py . [100%]
===================== 1 passed, 3 deselected in 0.12s ======================
那麼未標記的測試將不會執行。因此,這是一種將執行限制在特定測試的方法。
根據測試名稱自動新增標記¶
如果您有一個測試套件,其中測試函數名稱表示某種類型的測試,您可以實作一個掛鉤,自動定義標記,以便您可以使用 -m
選項。我們來看這個測試模組
# content of test_module.py
def test_interface_simple():
assert 0
def test_interface_complex():
assert 0
def test_event_simple():
assert 0
def test_something_else():
assert 0
我們想要動態定義兩個標記,並可以在 conftest.py
外掛程式中執行
# content of conftest.py
import pytest
def pytest_collection_modifyitems(items):
for item in items:
if "interface" in item.nodeid:
item.add_marker(pytest.mark.interface)
elif "event" in item.nodeid:
item.add_marker(pytest.mark.event)
我們現在可以使用 -m 選項
選擇一組
$ pytest -m interface --tb=short
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 4 items / 2 deselected / 2 selected
test_module.py FF [100%]
================================= FAILURES =================================
__________________________ test_interface_simple ___________________________
test_module.py:4: in test_interface_simple
assert 0
E assert 0
__________________________ test_interface_complex __________________________
test_module.py:8: in test_interface_complex
assert 0
E assert 0
========================= short test summary info ==========================
FAILED test_module.py::test_interface_simple - assert 0
FAILED test_module.py::test_interface_complex - assert 0
===================== 2 failed, 2 deselected in 0.12s ======================
或選擇「事件」和「介面」測試
$ pytest -m "interface or event" --tb=short
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 4 items / 1 deselected / 3 selected
test_module.py FFF [100%]
================================= FAILURES =================================
__________________________ test_interface_simple ___________________________
test_module.py:4: in test_interface_simple
assert 0
E assert 0
__________________________ test_interface_complex __________________________
test_module.py:8: in test_interface_complex
assert 0
E assert 0
____________________________ test_event_simple _____________________________
test_module.py:12: in test_event_simple
assert 0
E assert 0
========================= short test summary info ==========================
FAILED test_module.py::test_interface_simple - assert 0
FAILED test_module.py::test_interface_complex - assert 0
FAILED test_module.py::test_event_simple - assert 0
===================== 3 failed, 1 deselected in 0.12s ======================