Step 3
pytest · fixtures · parametrize
25 min
pytest · fixtures · parametrize
The de-facto Python test runner. Terser and more powerful than unittest.
1. Install
uv add --dev pytest pytest-asyncio pytest-cov
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
addopts = "-v --tb=short"
2. First test
def test_addition():
assert 1 + 1 == 2
3. parametrize
import pytest
@pytest.mark.parametrize("a, b, expected", [
(1, 1, 2),
(2, 3, 5),
(-1, 1, 0),
])
def test_add(a, b, expected):
assert a + b == expected
4. Fixtures
# tests/conftest.py
import pytest
from httpx import AsyncClient
@pytest.fixture
async def client():
from main import app
async with AsyncClient(app=app, base_url="http://test") as c:
yield c
async def test_create_user(client):
r = await client.post("/users", json={"email": "x"})
assert r.status_code == 201
5. Fixture scope
@pytest.fixture(scope="session")
def db_engine(): ...
@pytest.fixture(scope="function")
def db_session(db_engine): ...
Expensive resources in session, mutable state in function.
6. Async tests
import pytest
pytestmark = pytest.mark.asyncio
async def test_async_fn():
result = await async_compute()
assert result == 42
7. mocker — pytest-mock
def test_send_email(mocker):
mock = mocker.patch("app.mail.send_mail")
process_signup("user@example.com")
mock.assert_called_once_with("user@example.com", subject=mocker.ANY)
8. Coverage
uv run pytest --cov=app --cov-report=term-missing --cov-report=html
9. Gotchas
- Fixture name mismatch with parameter
- Mutating a session-scope fixture contaminates later tests
- Running async tests sync — configure
asyncio_modeor use the marker - Circular fixtures
10. Fast runs
pytest -x # stop on first failure
pytest -k "test_user" # name filter
pytest --lf # rerun last failed
pytest -n auto # parallel (pytest-xdist)
Closing
Fixtures + parametrize are what pull pytest ahead of vitest for backend work.
Next
- 04-testcontainers