How do you write and organize tests with pytest (fixtures, parametrize, marks)?
Quick Answer
pytest discovers tests as plain functions/methods named test_* (no boilerplate base class needed), and uses plain assert statements (rewriting them to show rich failure diffs). It provides fixtures (@pytest.fixture) for reusable setup/teardown injected by parameter name, @pytest.mark.parametrize to run the same test logic against many inputs, and marks (@pytest.mark.skip, .xfail, custom marks) to control test execution.
Detailed Answer
Plain functions, plain assert
# test_math.py
def add(a, b):
return a + b
def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_negative_numbers():
assert add(-1, -1) == -2
No self.assertEqual(...) boilerplate (as in unittest). pytest
rewrites plain assert statements at import time to produce detailed
failure output (showing both sides of a failed comparison) without any
special assertion methods.
Fixtures: reusable, composable setup/teardown
import pytest
@pytest.fixture
def db_connection():
conn = create_connection()
yield conn # provided to the test
conn.close() # teardown, runs after the test (even if it failed)
def test_query(db_connection): # requested by parameter name
result = db_connection.execute("SELECT 1")
assert result == 1
A fixture requested by a test function's parameter name is automatically
resolved, run, and injected by pytest. yield splits it into setup
(before) and teardown (after), with teardown guaranteed to run even if
the test fails. Fixtures can depend on other fixtures, be scoped
(scope="module", "session") to control how often they're recreated,
and be shared across a whole directory via a conftest.py.
parametrize: one test, many inputs
import pytest
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(-1, -1, -2),
(0, 0, 0),
])
def test_add(a, b, expected):
assert add(a, b) == expected
This runs test_add three times with three different argument sets,
reported as three separate test results — far more maintainable than
copy-pasting near-identical test functions for each input case, and each
case's failure is reported independently.
Marks: controlling test execution
@pytest.mark.skip(reason="not implemented yet")
def test_future_feature():
...
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX-only")
def test_unix_permissions():
...
@pytest.mark.xfail(reason="known bug, see #123")
def test_known_broken():
assert broken_function() == expected
skip/skipif exclude a test from the run entirely. xfail runs the
test but doesn't fail the suite if it fails as expected (and flags it if
it unexpectedly passes, via strict=True), useful for tracking known
issues without either deleting the test or leaving the suite red.
Organizing a test suite
tests/
conftest.py # shared fixtures, available to every test in this directory tree
test_models.py
test_views.py
integration/
test_api.py
conftest.py files are auto-discovered by pytest and their fixtures are
available to every test in the same directory and subdirectories,
without any import. This is the standard way to share setup logic across a
test suite.
pytest tests are plain assert-based
functions with no required base class. Fixtures provide composable,
scoped setup/teardown injected by parameter name. parametrize runs one
test body against many input sets as separate reported cases, and marks
(skip/skipif/xfail) control which tests run and how failures are
interpreted.