Chapter 4 · Organizing Tests and Marks
Chapter goals: organize tests sensibly with classes and modules, handle "can't run yet" cases with built-in markers (skip/xfail), and tag tests for classification with custom marks.
4.1 Organizing Tests into Classes
As your suite grows, cases for the same feature should live together. pytest supports grouping tests inside Test-prefixed classes; there are only two rules:
- The class name must start with
Testand must not define an__init__method (otherwise the whole class is skipped during collection); - Test methods start with
test_and need no base-class inheritance.
# tests/test_cart.py
class TestAddItem:
"""Cart add-item test group"""
def test_add_new_item(self):
cart = Cart()
cart.add("apple", qty=2)
assert cart.count("apple") == 2
def test_add_same_item_merges(self):
cart = Cart()
cart.add("apple", qty=1)
cart.add("apple", qty=3)
assert cart.count("apple") == 4
class TestRemoveItem:
def test_remove_existing(self):
...Why __init__ Is Forbidden
pytest's fixture mechanism (from Chapter 6 onward) replaces the traditional constructor-initialization approach; a Test class defining __init__ gets silently skipped — one of the most common "why aren't my tests running" causes among newcomers.
For module-level layout, the official docs recommend one of two layouts: tests separated from source (src/mypkg/ plus a top-level tests/) combined with an editable install (pip install -e .). New projects should also enable --import-mode=importlib in config to avoid clashes between same-named test files.
4.2 Built-in Marks: skip and skipif
A mark is metadata attached to a test function. The most-used family is "skipping":
import sys
import pytest
@pytest.mark.skip(reason="feature not implemented yet")
def test_future_feature():
...
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX platforms only")
def test_posix_paths():
...
# keeping conditions in module-level variables is easier to maintain
SUPPORTS_ORJSON = True
@pytest.mark.skipif(not SUPPORTS_ORJSON, reason="requires orjson")
def test_fast_json():
...Unconditional skipping uses skip; conditional uses skipif(cond, reason=...). Skipped tests show as s in reports along with your reason — always state why, since nobody remembers six months later.
Runtime dynamic skipping has its own tool: pytest.skip(), typically used inside fixtures when the environment turns out to be unsuitable.
4.3 Built-in Marks: xfail — Expected Failure
Some cases "should fail now but will be fixed later", e.g. waiting on an unmerged upstream bugfix. Hard-skipping hides the moment they actually get better; xfail keeps executing them:
@pytest.mark.xfail(reason="waiting on upstream fix #123")
def test_upstream_bug():
result = buggy_function()
assert result.correct
@pytest.mark.xfail(strict=True, reason="strict mode: passing means failure")
def test_will_be_fixed_soon():
...The outcome semantics are worth memorizing:
- Test fails →
xfailed(thexin reports): expected; - Test unexpectedly passes →
xpassed(theX); - With
strict=True, "unexpectedly passes" counts as a failure, forcing you to remove the mark as soon as the issue is fixed.
4.4 Custom Marks and Registration
A custom mark is just any decorator tagging tests with business labels:
# tests/test_payment.py
import pytest
@pytest.mark.slow
@pytest.mark.payment
def test_full_refund_flow():
...An unregistered mark triggers PytestUnknownMarkWarning on every use — that's a guard against typo'd mark names. The right move is registering them in your config file (Chapter 11 covers config files themselves):
# pyproject.toml
[pytest]
markers = [
"slow: slow tests, excluded daily via -m 'not slow'",
"payment: payment-domain cases",
]For stricter setups add --strict-markers (or configure strict_markers = true): using an unregistered mark then errors out instead of warning. Recommended for every serious project.
pytest --markers # list all registered marks and their descriptions
pytest -m slow # run only slow tests
pytest -m "not slow" # exclude slow tests4.5 Stacking Marks and Scopes
Marks stack freely and can also decorate whole classes or modules:
# class-level mark: applies to every test inside
@pytest.mark.slow
class TestEtlPipeline:
def test_extract(self):
...
def test_load(self):
...
# module-level mark: assigned to the conventional global pytestmark
pytestmark = pytest.mark.skipif(sys.version_info < (3, 10), reason="requires Python 3.10+")Two boundaries to note:
- Marks only affect tests; they have no effect on fixtures;
-mexpressions supportand / or / not, e.g.pytest -m "payment and not slow"precisely scopes a run — the core speed-up lever in day-to-day development on large projects.
4.6 Chapter Summary
Test-prefixed classes without__init__group tests; src/tests separation plus importlib import mode is the recommended setup for new projects;skip(reason=...)skips unconditionally,skipif(cond, reason=...)conditionally; both displays;xfailmeans "expected failure": failing yieldsxfailed, unexpected passing yieldsxpassed, andstrict=Trueescalates xpassed to FAILED;- Custom marks must be registered in config (or enable
--strict-markers) or you get warnings/errors; -m "not slow"is a daily-development efficiency weapon.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. Which class’s test_ methods will pytest NOT collect?
2. A test decorated @pytest.mark.xfail(strict=True) unexpectedly passes. What happens?
3. You use an unregistered custom mark @pytest.mark.slwo (typo). What happens by default?
4. Which command runs only payment-related tests while excluding slow ones?
🛠️ Hands-On Exercises
- Add two custom marks
slowandintegrationto your project, register them inpyproject.toml, then verifypytest -m "not slow"excludes them. - Write a time-dependent case and use
skipifto auto-skip it on weekends (hint:datetime.date.today().weekday() >= 5). - Convert a known environment-dependent failure into
xfail(strict=True), then satisfy the dependency and watch the enforcedxpassed→FAILEDreminder fire.
With organization and tagging covered, the next chapter introduces pytest's density multiplier: parametrization.