brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
pytestpytest is the standard Python test framework: plain assert statements, fixtures for setup and teardown, parametrized tests and a large plugin ecosystem. Use when a user asks to write unit tests, set up test fixtures, mock dependencies, run async tests, measure coverage, run tests in parallel, or practice test-driven development in Python.
Independent GitHub snapshot · Automated classification · Original documentation governs
pytest collects functions named test_* in files named test_*.py (or *_test.py), runs them, and rewrites plain assert statements so a failure shows both sides of the comparison. Fixtures provide setup and teardown, parametrize runs one test over many inputs, and plugins add async support, coverage, mocking and parallel runs. This skill targets pytest 9.x (checked against 9.1) with pytest-asyncio 1.x.
python -m venv .venv && source .venv/bin/activate
pip install pytest pytest-asyncio pytest-cov pytest-mock pytest-xdist
Put configuration in pyproject.toml ([tool.pytest.ini_options]) or, since pytest 9, in a native pytest.toml:
# pytest.toml
[pytest]
pythonpath = ["."] # lets tests import top-level modules such as `app`
testpaths = ["tests"]
asyncio_mode = "auto" # pytest-asyncio: no @pytest.mark.asyncio on every test
markers = ["slow: tests that take more than a second"]
strict = true # unknown markers and options become errors
Without pythonpath (or an installed package), from app import ... fails with ModuleNotFoundError when pytest is run from the project root.
# tests/test_users.py
import pytest
from app.services.users import create_user, validate_email
def test_create_user_returns_user_object():
user = create_user(name="Priya Nair", email="priya@acme.io")
assert user.name == "Priya Nair"
assert user.id is not None
def test_validate_email_rejects_invalid():
assert validate_email("not-an-email") is False
assert validate_email("user@") is False
class TestUserService:
"""A class only groups tests; no base class or self.assert* methods needed."""
def test_duplicate_email_raises(self):
create_user(name="Priya Nair", email="priya@acme.io")
with pytest.raises(ValueError, match="Email already exists"):
create_user(name="Tom Berg", email="priya@acme.io")
# tests/conftest.py — shared by every test in this directory and below
import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app.models import Base, User
@pytest_asyncio.fixture # async fixtures need this decorator in strict mode
async def db():
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async with async_sessionmaker(engine, expire_on_commit=False)() as session:
yield session # code after yield is the teardown
await engine.dispose()
@pytest_asyncio.fixture
async def sample_user(db):
user = User(name="Priya Nair", email="priya@acme.io", role="member")
db.add(user)
await db.commit()
return user
@pytest.fixture
def api_client(db):
from fastapi.testclient import TestClient
from app.main import app
from app.dependencies import get_db
app.dependency_overrides[get_db] = lambda: db
yield TestClient(app)
app.dependency_overrides.clear()
That example needs pip install "sqlalchemy[asyncio]" aiosqlite. Built-in fixtures worth knowing: tmp_path (a fresh directory per test), monkeypatch (set env vars and attributes, undone automatically), capsys and caplog (captured output and log records). A fixture's scope can be function (default), class, module, package or session; widen it only for expensive, read-only resources.
@pytest.mark.parametrize(
"plan,expected_price",
[("free", 0), ("starter", 29), ("pro", 79), pytest.param("enterprise", 199, id="enterprise-list-price")],
)
def test_calculate_price(plan, expected_price):
assert calculate_price(plan) == expected_price
pytest 9 also ships subtests (a subtests fixture) for looping over cases inside one test without stopping at the first failure:
def test_slugify_variants(subtests):
for raw, slug in [("Hello World", "hello-world"), (" Spaces ", "spaces")]:
with subtests.test(raw=raw):
assert slugify(raw) == slug
# tests/test_notifications.py — pytest-mock provides the `mocker` fixture
from unittest.mock import AsyncMock
async def test_send_welcome_email(mocker, sample_user):
send = mocker.patch("app.services.email.send_email", new_callable=AsyncMock,
return_value={"id": "msg_8841"})
result = await send_welcome_email(sample_user.id)
send.assert_awaited_once_with(to="priya@acme.io", subject="Welcome!", template="welcome")
assert result["id"] == "msg_8841"
Patch the name where it is looked up (app.services.notifications.send_email, if that module did from ... import send_email), not where it is defined.
pytest # everything under testpaths
pytest tests/test_users.py::test_validate_email_rejects_invalid # one test
pytest -x --lf # stop at first failure; rerun last failures
pytest -k "create and not duplicate" # select by expression
pytest -m "not slow" # select by marker
pytest --cov=app --cov-report=term-missing # coverage (pytest-cov)
pytest -n auto # parallel workers (pytest-xdist)
pytest -ra # summary of skipped/xfailed reasons
User request: "Write pytest tests for calculate_discount in app/pricing.py, including bad input."
# tests/test_pricing.py
import pytest
from app.pricing import calculate_discount
@pytest.mark.parametrize("total,code,expected", [(100.0, "SPRING10", 90.0), (100.0, None, 100.0), (0.0, "SPRING10", 0.0)])
def test_discount_applies(total, code, expected):
assert calculate_discount(total, code) == pytest.approx(expected)
def test_unknown_code_raises():
with pytest.raises(ValueError, match="Unknown discount code"):
calculate_discount(50.0, "NOPE")
Run pytest tests/test_pricing.py -q; expect 4 passed (three parametrized cases plus the error test).
User request: "My async tests fail with 'async def functions are not natively supported'."
pytest does not run coroutines on its own. Install the plugin and set the mode:
pip install pytest-asyncio
Add asyncio_mode = "auto" to pytest.toml, or keep the default strict mode and decorate each test with @pytest.mark.asyncio and each async fixture with @pytest_asyncio.fixture. Re-run pytest -q: the tests now pass.
assert; use pytest.approx for floats and pytest.raises(..., match=...) for exceptions.@pytest.fixture errors with "requested an async fixture ... no plugin or hook that handled it"; use @pytest_asyncio.fixture or switch to asyncio_mode = "auto".yield clean up even when the test fails; no try/finally needed.strict = true (or --strict-markers) a typo fails at collection instead of silently deselecting tests.-n auto) need tests that do not share files, ports or database rows.unittest.TestCase classes also run under pytest, but fixtures and parametrize do not apply to their methods.