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.tomlat the project root. Without one,pip install pytest pytest-covand put the options inpytest.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 stateThe 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=80term-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 -3Collection lists every test you expect; the exit code is 0.
Common errors
ModuleNotFoundError: No module named 'myapp'. The package is not installed; runpip install -e ..- Fixtures not found.
conftest.pyis outside the test directory tree. - A test file is not collected. It does not match
test_*.py, or the function does not start withtest_. - Coverage is 0%.
--covnames 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.