使用自訂標記¶
以下是一些使用如何使用屬性標記測試函數機制的範例。
標記測試函數並選擇它們來執行¶
您可以像這樣使用自訂元數據「標記」測試函數
# content of test_server.py
import pytest
@pytest.mark.webtest
def test_send_http():
pass # perform some webtest test for your app
@pytest.mark.device(serial="123")
def test_something_quick():
pass
@pytest.mark.device(serial="abc")
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 ======================
此外,您可以限制測試執行,使其僅執行符合一個或多個標記關鍵字引數的測試,例如,僅執行標記為 device
和特定 serial="123"
的測試
$ pytest -v -m "device(serial='123')"
=========================== 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_something_quick PASSED [100%]
===================== 1 passed, 3 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
將選擇類別上的所有測試方法。節點也會為參數化 fixture 或測試的每個參數建立,因此選擇參數化測試必須包含參數值,例如 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” 和 “quick” 測試
$ 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
還會匹配測試父項的名稱(通常是檔案和類別的名稱)、在測試函數上設定的屬性、套用至測試或其父項的標記,以及明確新增至測試或其父項的任何額外 關鍵字
。
註冊標記¶
為您的測試套件註冊標記很簡單
# 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
在使用參數化時標記個別測試¶
當使用參數化時,套用標記會使其套用至每個個別測試。但是,也可以將標記套用至個別測試實例
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」標記僅套用至第二個測試。Skip 和 xfail 標記也可以以這種方式套用,請參閱使用參數化進行 Skip/xfail。
自訂標記和命令列選項來控制測試執行¶
外掛程式可以提供自訂標記,並根據其標記實作特定行為。這是一個獨立的範例,新增了一個命令列選項和一個參數化測試函數標記,以執行透過具名環境指定的測試
# 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 ======================
那麼未標記的測試將不會執行。因此,這是一種將執行限制為特定測試的方法。
根據測試名稱自動新增標記¶
如果您有一個測試套件,其中測試函數名稱指示某種類型的測試,您可以實作一個 hook,該 hook 自動定義標記,以便您可以使用 -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 ======================
或選擇 “event” 和 “interface” 測試
$ 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 ======================