How do you write and organize tests with pytest (fixtures, parametrize, marks)?

7 minintermediatetestingpytestfixtures

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.

Related Resources