Markers are labels you attach to tests with @pytest.mark. Some built-in markers change how a test runs: skip it, skip it on certain systems, or expect it to fail. Custom markers such as smoke or regression let you run exactly the group of tests you need, for example a quick smoke suite on every commit and the full regression suite at night. In this lesson you will use all of them, register your own markers properly and select tests with -m expressions.
📖 What You’ll Learn: skip and skipif with reasons, skipping from inside a test, xfail and the difference between xfailed and xpassed, strict xfail, custom markers like smoke and regression, registering markers in pytest.ini, –strict-markers, selecting tests with -m and boolean expressions, listing markers, and marking whole classes and modules.
🏷️ What Is a Marker?
A marker is a decorator of the form @pytest.mark.<name> placed above a test (or a class). pytest uses markers in two ways:
- Built-in markers change execution:
skip,skipif,xfail,parametrize,usefixtures,filterwarnings. - Custom markers are labels you invent, such as
smoke,regressionorslow, used to select tests with-m.
⏭️ skip, skipif and xfail
Create tests/test_skip_xfail.py:
📄 tests/test_skip_xfail.py
import sys
import pytest
@pytest.mark.skip(reason="payment page is being redesigned")
def test_payment_form():
assert False # never runs
@pytest.mark.skipif(sys.platform == "win32", reason="uses a Linux-only path")
def test_linux_path():
assert sys.platform != "win32"
def test_skip_from_inside():
feature_enabled = False # imagine reading a feature flag
if not feature_enabled:
pytest.skip("feature flag 'new-cart' is off")
assert True
@pytest.mark.xfail(reason="bug #42: total ignores discounts")
def test_discount_total():
assert 100 - 10 == 100 # fails: expected while the bug exists
@pytest.mark.xfail(reason="bug #57: badge count off by one")
def test_badge_count():
assert 1 + 1 == 2 # passes unexpectedly: XPASS
| Marker | Effect |
|---|---|
@pytest.mark.skip(reason=...) | Never runs the test. Always give a reason. |
@pytest.mark.skipif(condition, reason=...) | Skips only when the condition is true, e.g. a platform or Python version. |
pytest.skip("...") inside a test | Skips at runtime, after checking something such as a feature flag or environment. |
@pytest.mark.xfail(reason=...) | Runs the test but expects it to fail. A failure is reported as xfailed; an unexpected pass as xpassed. |
Run it with -rsxX to see a summary of skips (s), xfails (x) and xpasses (X):
⌨️ Terminal
pytest -q -rsxX tests/test_skip_xfail.py
🖥️ Output
sssxX [100%]
=========================== short test summary info ===========================
SKIPPED [1] tests\test_skip_xfail.py:6: payment page is being redesigned
SKIPPED [1] tests\test_skip_xfail.py:11: uses a Linux-only path
SKIPPED [1] tests\test_skip_xfail.py:19: feature flag 'new-cart' is off
XFAIL tests/test_skip_xfail.py::test_discount_total - bug #42: total ignores discounts
XPASS tests/test_skip_xfail.py::test_badge_count - bug #57: badge count off by one
3 skipped, 1 xfailed, 1 xpassed in 0.05s
(This run was on Windows, so test_linux_path was skipped; on Linux it would pass.)
💡 Tip: Use xfail for known bugs rather than skip. The test keeps running, so the day the bug is fixed you see XPASS and know to remove the marker. A skipped test tells you nothing.
🔒 Strict xfail
By default an XPASS does not fail the run, so a fixed bug can go unnoticed. With strict=True, an unexpected pass fails the test (tests/test_strict_xfail.py):
📄 tests/test_strict_xfail.py
import pytest
@pytest.mark.xfail(reason="bug #57: badge count off by one", strict=True)
def test_badge_count_strict():
assert 1 + 1 == 2
⌨️ Terminal
pytest -q -rX tests/test_strict_xfail.py
🖥️ Output
F [100%]
================================== FAILURES ===================================
___________________________ test_badge_count_strict ___________________________
[XPASS(strict)] bug #57: badge count off by one
1 failed in 0.02s
To make every xfail strict, set xfail_strict = true in pytest.ini. Other useful xfail options: raises=SomeError (only that exception counts as the expected failure) and run=False (do not even run it, for example when it would hang).
🏷️ Custom Markers: smoke, regression, slow
Custom markers group tests by purpose. First register them in pytest.ini, with a short description each:
📄 pytest.ini
[pytest]
markers =
smoke: quick checks of the most important flows (run on every commit)
regression: full functional coverage (run nightly)
slow: tests that take longer than a few seconds
Then label the tests (tests/test_suite.py):
📄 tests/test_suite.py
import pytest
from playwright.sync_api import Page, expect
@pytest.mark.smoke
@pytest.mark.regression
def test_login_page_loads(page: Page):
page.goto("https://sqatools.in/login-page/")
expect(page.get_by_role("button", name="Log in")).to_be_visible()
@pytest.mark.regression
def test_practice_page_table(page: Page):
page.goto("https://sqatools.in/automation-practice-page/")
expect(page.locator("table tbody tr")).to_have_count(3)
@pytest.mark.regression
@pytest.mark.slow
def test_practice_page_all_headings(page: Page):
page.goto("https://sqatools.in/automation-practice-page/")
expect(page.get_by_role("heading", name="Bottom of Page")).to_be_visible()
A test can carry several markers. Now select groups with -m:
⌨️ Terminal
pytest -q -m smoke tests/test_suite.py
🖥️ Output
. [100%]
1 passed, 2 deselected, 1 warning in 3.82s
(The warning is the pytest-playwright base_url notice from Lesson 2.)
-m accepts boolean expressions with and, or, not and parentheses:
⌨️ Terminal
pytest -q -m "regression and not slow" tests/test_suite.py
🖥️ Output
.. [100%]
2 passed, 1 deselected, 1 warning in 5.02s
| Command | Runs |
|---|---|
pytest -m smoke | Tests marked smoke |
pytest -m "smoke or regression" | Tests with either marker |
pytest -m "regression and not slow" | Regression tests except slow ones |
pytest -m "not slow" | Everything except slow tests |
📝 Note: -m selects by marker, -k selects by name. You can combine them: pytest -m regression -k login.
⚠️ Typos in Marker Names: Register and Use –strict-markers
pytest does not know whether @pytest.mark.smoek is a typo or a new marker. Without registration it only warns (unregistered/test_typo.py):
📄 unregistered/test_typo.py
import pytest
@pytest.mark.smoek # typo of "smoke"
def test_typo():
assert True
⌨️ Terminal
pytest -q unregistered/test_typo.py
🖥️ Output
C:\pwv\pytest-markers\unregistered\test_typo.py:4: PytestUnknownMarkWarning: Unknown pytest.mark.smoek - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
@pytest.mark.smoek # typo of "smoke"
1 passed, 1 warning in 0.01s
The test still ran, but pytest -m smoke would silently skip it. With --strict-markers, an unregistered marker is an error instead:
⌨️ Terminal
pytest -q --strict-markers unregistered/test_typo.py
🖥️ Output
=================================== ERRORS ====================================
'smoek' not found in `markers` configuration option
=========================== short test summary info ===========================
ERROR unregistered/test_typo.py - Failed: 'smoek' not found in `markers` conf...
!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.07s
The run stops during collection with exit code 2, so the typo cannot go unnoticed.
Add --strict-markers to addopts in pytest.ini so typos can never slip through:
[pytest]
addopts = --strict-markers
markers =
smoke: quick checks of the most important flows
📋 Listing Markers
pytest --markers lists every marker available: your registered ones (with their descriptions) first, then those added by plugins, then pytest’s built-in ones. With pytest-playwright installed the list starts like this:
🖥️ Output
@pytest.mark.smoke: quick checks of the most important flows (run on every commit)
@pytest.mark.regression: full functional coverage (run nightly)
@pytest.mark.slow: tests that take longer than a few seconds
@pytest.mark.skip_browser(name): mark test to be skipped a specific browser
@pytest.mark.only_browser(name): mark test to run only on a specific browser
@pytest.mark.browser_context_args(**kwargs): provide additional arguments to browser.new_context()
@pytest.mark.skip(reason=None): skip the given test function with an optional reason. ...
You will use skip_browser, only_browser and the browser_context_args marker in Module 6.
📦 Marking a Whole Class or Module
- A class: put the marker above the class and every test method inherits it.
- A whole file: set a module-level variable
pytestmark = pytest.mark.regression(or a list of markers) at the top of the test file.
import pytest
pytestmark = [pytest.mark.regression] # applies to every test in this file
@pytest.mark.smoke
class TestCheckout:
def test_shipping_form(self, page): # marked regression AND smoke
...
❓ Frequently Asked Questions
🤔 Should I delete skipped tests?
Not immediately, but review them regularly. A test skipped for months is usually dead code. Prefer xfail for known bugs so you notice when they are fixed.
🤔 What is the difference between -m and -k?
-m selects tests by marker expression; -k selects by test name (including parametrize IDs). Both accept and, or and not.
🤔 Can I skip a test only in one browser?
Yes, with pytest-playwright’s @pytest.mark.skip_browser("webkit"), or check the browser_name fixture inside the test and call pytest.skip(). Module 6 covers both.
🤔 Why use reason= everywhere?
Reasons appear in reports and in the -rs summary, so whoever reads the results knows why a test did not run without opening the code.
🏁 Summary
skip,skipifandpytest.skip()stop tests from running, with a reason;xfailexpects failure and reportsxfailedorxpassed.strict=True(orxfail_strict = true) turns an unexpected pass into a failure.- Register custom markers in pytest.ini, add
--strict-markers, and select groups with-mexpressions. - Mark whole classes with a decorator and whole files with
pytestmark.
Next, you will put all of pytest’s settings together in pytest.ini or pyproject.toml.
🎬 Watch the Playwright tutorial series on YouTube!
Subscribe to SqaToolsByDY • Practice on the SQA Tools Automation Practice Page