Python · Testing with pytest · Intermediate
Marks and Test Selection
Tag tests with built-in and custom marks to control which tests run.
Quick topic start and explanations before exercises (exercises below):
Marks in Practice
#Marks Quick Reference
#Exercises:
Skip a Test with a Reason
#Write a test function `test_future_feature` that would test some not-yet-implemented functionality. Mark it with `@pytest.mark.skip` and a descriptive `reason` string. Run `pytest -v` and confirm the test shows as `SKIPPED` with your reason visible. Then run `pytest -v -rs` to see the skip reason in the summary section.
import pytest
@pytest.mark.skip(reason='...') # add your reason here
def test_future_feature():
# this code never runs when skipped
result = some_unimplemented_function()
assert result == 42
Solution
import pytest
@pytest.mark.skip(reason='payment refunds not implemented yet — see issue #42')
def test_future_feature():
result = some_unimplemented_function()
assert result == 42
Skip Based on Python Version
#Write a test that uses `@pytest.mark.skipif` to skip if the Python version is below 3.10. Inside the test body, use `match` statement syntax (added in Python 3.10): `match value: case 1: result = "one"`. Run `pytest -v` — the test should run on Python 3.10+ and be skipped on earlier versions. Print `sys.version` in the test to confirm which version ran it.
import sys
import pytest
@pytest.mark.skipif(
# condition: sys.version_info < (?, ?),
# reason='...',
)
def test_match_statement():
value = 1
# use a match/case statement here
# assert result == 'one'
Solution
import sys
import pytest
@pytest.mark.skipif(
sys.version_info < (3, 10),
reason='match statement added in Python 3.10',
)
def test_match_statement():
print(f'running on Python {sys.version}')
value = 1
match value:
case 1:
result = 'one'
case _:
result = 'other'
assert result == 'one'
Mark a Test as Expected to Fail (xfail)
#Write two tests: `test_known_bug` and `test_known_bug_strict`. Both call `int("abc")` without try/except (this raises `ValueError`). Mark `test_known_bug` with `@pytest.mark.xfail` and `test_known_bug_strict` with `@pytest.mark.xfail(strict=True)`. Run `pytest -v` — both should show as `XFAIL`. Notice the difference: without `strict`, an unexpected pass would show as `XPASS` (warning but not a failure); with `strict=True`, an unexpected pass becomes a hard `FAILED` that blocks CI.
import pytest
@pytest.mark.xfail(reason=...) # fill in the reason
def test_known_bug():
result = int('abc')
assert result == 0
@pytest.mark.xfail(strict=..., reason=...) # add strict= parameter
def test_known_bug_strict():
result = int('abc')
assert result == 0
Solution
import pytest
@pytest.mark.xfail(reason='bug #77: int() does not handle letters')
def test_known_bug():
result = int('abc')
assert result == 0
# With strict=True: if this test unexpectedly passes, pytest reports it as FAILED
@pytest.mark.xfail(strict=True, reason='bug #77')
def test_known_bug_strict():
result = int('abc')
assert result == 0
Register a Custom Mark and Filter with -m
#Add `slow` to the `markers` section in `pytest.ini`. Write three test functions in one file: `test_fast_calculation` (no mark, just `assert 2 + 2 == 4`), `test_slow_api_call` and `test_slow_db_query` (both marked `@pytest.mark.slow`). Run `pytest -m slow -v` and confirm only the two slow tests run. Run `pytest -m "not slow" -v` and confirm only the fast test runs.
# pytest.ini
# [pytest]
# markers =
# slow: marks tests as slow (deselect with '-m "not slow"')
# test_mixed.py
import pytest
def test_fast_calculation():
assert 2 + 2 == 4
@pytest.mark.slow
def test_slow_api_call():
# simulate a slow operation
import time
time.sleep(0.1)
assert True
@pytest.mark.slow
def test_slow_db_query():
import time
time.sleep(0.1)
assert True
Solution
# pytest.ini
# [pytest]
# markers =
# slow: marks tests as slow (deselect with '-m "not slow"')
# test_mixed.py
import pytest
def test_fast_calculation():
assert 2 + 2 == 4
@pytest.mark.slow
def test_slow_api_call():
import time
time.sleep(0.1)
assert True
@pytest.mark.slow
def test_slow_db_query():
import time
time.sleep(0.1)
assert True
Combine Multiple Marks
#Register both `slow` and `unix_only` marks in `pytest.ini`. Write `test_fast_check` with no marks. Write `test_file_permissions` marked with `@pytest.mark.slow`, `@pytest.mark.unix_only`, AND `@pytest.mark.skipif(sys.platform == 'win32', ...)`. Run `pytest -v` to confirm both tests execute (or skip on Windows). Then run `pytest -m slow -v` — only the slow test is selected. Then run `pytest -m unix_only -v` — same result: only the platform test is selected. Notice that `@pytest.mark.skipif` provides a runtime guard, while the mark enables CLI filtering.
# pytest.ini
# [pytest]
# markers =
# slow: slow tests
# unix_only: runs only on Unix/Linux/macOS
import sys
import pytest
def test_fast_check():
assert 'py' in 'pytest'
@pytest.mark.slow
@pytest.mark.unix_only
@pytest.mark.skipif(sys.platform == 'win32', reason='uses POSIX file permissions')
def test_file_permissions():
import os
import tempfile
with tempfile.NamedTemporaryFile() as f:
os.chmod(f.name, 0o644)
mode = oct(os.stat(f.name).st_mode)[-3:]
assert mode == '644'
Solution
# pytest.ini
# [pytest]
# markers =
# slow: slow tests
# unix_only: runs only on Unix/Linux/macOS
import sys
import pytest
def test_fast_check():
assert 'py' in 'pytest'
@pytest.mark.slow
@pytest.mark.unix_only
@pytest.mark.skipif(sys.platform == 'win32', reason='uses POSIX file permissions')
def test_file_permissions():
import os
import tempfile
with tempfile.NamedTemporaryFile() as f:
os.chmod(f.name, 0o644)
mode = oct(os.stat(f.name).st_mode)[-3:]
assert mode == '644'