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), uses plain `assert` statements (rewriting them to show rich failure diffs), and 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 — the standard way to share setup logic across a
test suite.
Interview-ready summary: 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.