Overview

A clean pytest setup keeps configuration in pyproject.toml, shared fixtures in conftest.py, and coverage behind pytest-cov. This guide goes from an empty tests/ folder to a passing suite with a coverage report. Testing strategy is in testing and python-testing; FastAPI-specific tests are in write-tests-for-fastapi.

Prerequisites

  • Python 3.10 or newer, and an activated virtual environment (python -m venv .venv && source .venv/bin/activate).
  • A pyproject.toml at the project root. Without one, pip install pytest pytest-cov and put the options in pytest.ini.

Steps

1. Declare test dependencies and options

[project.optional-dependencies]
dev = ["pytest>=8", "pytest-cov>=5"]
 
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers"
pip install -e ".[dev]"

The editable install makes import myapp work from tests. -ra prints a summary of skipped and failed tests; --strict-markers fails on a misspelled marker. Add pytest-asyncio and asyncio_mode = "auto" only if you test async code.

2. Add a conftest.py

pytest loads tests/conftest.py automatically; do not import it.

# tests/conftest.py
import pytest
 
@pytest.fixture
def sample_user() -> dict:
    return {"id": 1, "name": "Alice", "email": "alice@example.com"}

3. Write the first test

Name files test_*.py and functions test_*; request fixtures by parameter name.

# tests/test_users.py
from myapp.users import display_name
 
def test_display_name_uses_name(sample_user):
    assert display_name(sample_user) == "Alice"
 
def test_display_name_falls_back_to_email():
    assert display_name({"id": 2, "email": "bob@example.com"}) == "bob@example.com"

4. Scope fixtures for setup and teardown

@pytest.fixture(scope="session")
def db():
    conn = connect_test_db()
    yield conn            # runs after the last test that uses it
    conn.close()
 
@pytest.fixture
def tx(db):
    trans = db.begin()
    yield db
    trans.rollback()      # each test starts from a clean state

The default scope is function. Use session for expensive resources and a per-test transaction rollback for isolation; see set-up-postgres-locally for a database to connect to.

5. Check discovery, then run with coverage

pytest --collect-only -q
pytest --cov=myapp --cov-report=term-missing --cov-fail-under=80

term-missing lists uncovered lines. --cov-fail-under makes CI fail below the floor, so pick a number your suite already meets and ratchet it up.

Verify it worked

pytest -q; echo "exit=$?"          # 0 when all pass
pytest --collect-only -q | tail -3

Collection lists every test you expect; the exit code is 0.

Common errors

  • ModuleNotFoundError: No module named 'myapp'. The package is not installed; run pip install -e ..
  • Fixtures not found. conftest.py is outside the test directory tree.
  • A test file is not collected. It does not match test_*.py, or the function does not start with test_.
  • Coverage is 0%. --cov names a different package than the one imported; match the import name.
  • Tests pass locally and fail in CI. Environment variables or services are missing in the CI job.

FAQ

Where should pytest configuration live?

Put it under [tool.pytest.ini_options] in pyproject.toml, with testpaths = ["tests"]; use pytest.ini only when the project has no pyproject.toml.

Do I need to import conftest.py in my tests?

No; pytest loads tests/conftest.py automatically, and tests request its fixtures by parameter name.

How do I fix ModuleNotFoundError when running pytest?

The package is not installed; run pip install -e . (or pip install -e ".[dev]") so import myapp works from tests.

What coverage threshold should I set with pytest-cov?

Pick a --cov-fail-under number your suite already meets, such as 80, and ratchet it up over time.