The suite is fully asynchronous (pytest-asyncio, asyncio_mode = auto). Pytest collects tests natively from backend/src/ (testpaths = src, --import-mode=importlib): domain tests under apps/<app>/<subapp>/tests/ and infra unit tests under core/tests/. Plugin: backend/conftest.py (pytest_plugins = ["tests.conftest"]). There is no aggregator file.
Classification is explicit on each test module:
pytestmark = pytest.mark.integration # HTTP; file must be named *_vN.py
pytestmark = pytest.mark.unit # isolated; no *_vN.py suffixMarkers come from pytestmark, not from folders. Do not create tests/unit/ or tests/integration/. Do not put test_*.py in backend/tests/ (that directory is only conftest.py and helper.py).
HTTP tests use a session-scoped httpx.AsyncClient against Testcontainers PostgreSQL (pgvector) and Redis. Unit tests must not need Docker.
- Poetry and the backend virtualenv (
cd backend && poetry install). - Docker only for integration (or a full
pytest -vwith no-m): Docker Desktop or Engine must be running so Testcontainers can start Postgres and Redis.
From the backend directory:
poetry run pytest -v
poetry run pytest -m unit -v
poetry run pytest -m integration -v
poetry run pytest -v --cov --cov-report=term-missing --cov-fail-under=80
poetry run mypy src
poetry run ruff format --check .
poetry run ruff check src
poetry run pytest src/apps/<app>/<subapp>/tests/ -v
poetry run pytest src/apps/<app>/<subapp>/tests/test_v1.py -v
poetry run pytest src/core/tests/ -vFrom the repository root, the same commands are available under Project Tools: python setup.py tools (or python3 setup.py → option 2).
pytest -m unit is what pre-commit runs. It must pass without Docker and without --cov.
The coverage gate (--cov-fail-under=80) applies only to the full suite. Do not put --cov in pytest.ini addopts. Config lives in [tool.coverage.*] in pyproject.toml (source = ["src"]; tests and Alembic revisions are omitted).
Do not use poetry run pytest tests/ as the full suite. That directory has no test_*.py.
Without Docker, requesting integration still aborts (pytest.exit). That is intentional: a green commit must not skip HTTP by accident.
- Domain tests live next to the subapp:
src/apps/<app>/<subapp>/tests/. Infra unit tests live insrc/core/tests/(alwayspytestmark = unit). - HTTP:
test_v1.py(andtest_v2.pywhen that API exists). Each test creates its own rows (uuid4()in unique fields). No module-levelglobalIDs, no file-order coupling. - Service unit:
test_<resource>_service.pywithAsyncMockrepositories (see.agents/examples/subapp/tests/test_item_service.py). Cover every domainraiseand a happy path per public method — not only the firstif. - Fixtures:
client(session, HTTP only),admin_headers(function),settings(lazy import — do not importsrc.core.config.settingsat the top of a test module). - Do not mutate
client.cookies. The session cookie jar ignoresSet-Cookieso login does not leak across tests. Do notflushdbthe shared Redis. - Cover 401 (missing token) and 403 (
/dbas a non-superuser) in addition to the happy-path CRUD. HTTP errors are RFC 9457 Problem Details: assertproblem_body(...)fromsrc.core.exceptions.problem(not FastAPI's{"detail": ...}). - Tests share one DB for the run. Do not permanently mutate core seed data (default tier, first admin, system superuser). Rate-limit HTTP tests must use a disposable tier.
The mold .agents/examples/subapp/tests/ is not collected.
From backend/:
poetry run mypy srcbackend/mypy.ini sets disallow_untyped_defs and check_untyped_defs for src/ (apps/, core/, _overrides/), with the SQLAlchemy mypy plugin. Tests and Alembic revisions are excluded. This is not mypy --strict. SQLModel/SQLAlchemy stub mismatches (Field(), exec(), Row["col"], ConfigDict) stay listed in disable_error_code with a comment — var-annotated is left on. _overrides/pydantic/optional.py is ignored as a whole (create_model).
.github/workflows/tests.yml runs on push/PR:
- checks (no Docker):
ruff format --check .,ruff check src,mypy src,pytest -m unit -v - test (Testcontainers):
pytest -v --cov --cov-report=term-missing --cov-fail-under=80
Dummy SECRET_KEY and broker settings come from the pytest plugin — CI does not need repository secrets. Dependabot updates pip (/backend) and GitHub Actions weekly; those PRs squash-merge when jobs checks and test pass (repo must allow auto-merge and require those checks on main).