Test system overview¶
Concise reference for developers: how pytest is wired, where shared fixtures live, and how configuration is supplied in tests.
Running tests¶
Use the project’s package manager:
uv run pytest # whole tree (from repo root)
uv run pytest src/listener/tests/test_listen_unit.py
uv run pytest src/tests/test_environment.py -q
pyproject.toml sets asyncio_mode = auto and pytest_env sets APP_ENV=test plus LISTENER_SR_URL_LEAGUE=mlb (so get_listener_environment() works without a repo-root .env).
Why tests see the Postgres container¶
Pytest only loads conftest.py files on the path from each test file up to the root. src/tests/conftest.py is not an ancestor of src/listener/tests/, so it would not load for listener-only runs.
The repo-root conftest.py fixes that with:
That registers src/tests/conftest.py as a plugin for every run that uses this rootdir. Session infrastructure (below) therefore runs even when you collect only src/listener/tests/ or src/api/tests/.
Shared plugin: src/tests/conftest.py¶
| Mechanism | Role |
|---|---|
pytest_configure |
Celery: task_always_eager and task_eager_propagates so tasks run inline in tests (see Celery). Also mkdirs repo-root data/listener so relative FILESYSTEM_CONTAINER_DATA_DIR tests work on a clean checkout. |
container_db_url (session) |
Starts a Postgres testcontainer (pgvector/pgvector:pg17), sets DATABASE_URL via MonkeyPatch for the session, and points db_sessionmanager at that URL. |
container_db (session, autouse) |
Creates DB, runs init_database (SQLModel metadata create_all), yields, then drops the DB. |
container_redis (session, autouse) |
Starts Redis (redis:7-alpine) and sets REDIS_URL via MonkeyPatch. Isolated from host/compose Redis (do not use Scarlett staging Redis). |
db (function) |
Async SQLAlchemy session bound to a connection inside a transaction that rolls back after each test. |
factories (function) |
Async Factory Boy factories tied to that same AsyncSession (see below). |
localstack_container (session) |
Optional LocalStack + S3; sets AWS env vars for tests that request this fixture. |
Anything that needs the real async DB in a test should use the db fixture (or the same patterns as existing API/lib tests). Listener unit tests that only exercise _process_event with mocks may not request db but still pay the session cost of container_db because it is autouse.
Factory Boy and the factories fixture¶
Async tests use the db fixture, which opens an outer transaction and binds the session with join_transaction_mode="create_savepoint". Commits on that session end a savepoint, not the outer transaction, so everything still rolls back when the test finishes.
Test data for League / Team / Match should be created with the factories fixture so Factory Boy uses that same AsyncSession (async-factory-boy AsyncSQLAlchemyFactory). The bundle is returned by bind_async_sqlmodel_factories(db) in tests.factories (src/tests/factories.py). Do not add rows through a different session or connection, or isolation breaks.
Usage:
- Request
factoriesand callawait factories.league.create(...),await factories.team.create(...),await factories.match.create(...), orawait factories.match.create_with_teams(...)for league + two teams + match in one step.
Code reference: src/tests/conftest.py (factories fixture docstring), src/tests/factories.py.
Test layout (high level)¶
| Area | Typical path | Notes |
|---|---|---|
| Shared DB/Celery/LocalStack | src/tests/conftest.py |
Loaded globally via pytest_plugins. |
| App / plumbing | src/tests/ |
e.g. test_plumbing.py, test_environment.py. |
| Listener | src/listener/tests/ |
test_listen_unit.py (in-process), test_listen.py (script subprocess), stages/ (mirrors listener/lib/stages/), conftest.py for listener fixtures. |
| Library | src/lib/tests/ |
Often nested by domain. |
| API | src/api/tests/ |
May define local conftest.py. |
Configuration in tests: get_base_environment / get_listener_environment¶
Settings classes use Pydantic Settings. Factories wrap the constructors:
get_base_environment(**overrides)→BaseEnvironment(**overrides)get_listener_environment(**overrides)→MatchListenerEnvironment(**overrides)
Precedence (highest first): keyword arguments → process environment → env files → field defaults. See Pydantic field value priority.
In tests, prefer explicit overrides for the fields you care about instead of relying on a full .env. Remaining fields still resolve from env/files (e.g. DATABASE_URL once the session container has set it).
Daily CI¶
Workflow: .github/workflows/staging-daily-pytest.yml (Staging daily pytest) on Scarlett (self-hosted, macOS ARM64). Always checks out staging.
- Schedule:
0 6 * * *(06:00 UTC = 2am EDT / 1am EST). GitHub cron is UTC-only. - Manual: Actions → Staging daily pytest → Run workflow (still tests
staging, not the branch you pick). - Command:
uv run pytest --junitxml=pytest-results.xmlfrom repo root (same default suite as above;-m 'not external'viapyproject.toml). The jobmkdirs$GITHUB_WORKSPACE/data/listenerand sets placeholderDATABASE_URL/REDIS_URL/SPORTRADAR_API_KEY/ AWS keys /FILESYSTEM_CONTAINER_DATA_DIRso import-timeBaseEnvironmentcan construct (no repo-root.envon a clean checkout). Testcontainers still replaceDATABASE_URLandREDIS_URL. Do not source.env.staging(that would point tests at staging Postgres/Redis).LISTENER_SR_URL_BASEis set to the staging srsim (https://pr-srsim-staging.jvsassoc.com);requires_srsimtests skip when that host is unreachable.pytest_envsuppliesLISTENER_SR_URL_LEAGUE. - Success: silent. Failure or timeout (90m): Slack root (
staging, short SHA, Open Actions run + Fix with AI) plus a thread listing failing tests from junit. Artifactpytest-results. See STAGING.md. - Reuses Actions secret
SLACK_BOT_TOKENand variable or secretSLACK_CHANNEL_ID(same as staging release notes). - GitHub registers the cron from the default branch workflow file (
develop); the job then checks outstaging. The YAML must exist ondevelopbefore the nightly job fires.
Related docs¶
- Celery — broker, result backend, autodiscovery, and why tests use eager execution.
- Environment configuration — env files,
BaseEnvironment,MatchListenerEnvironment. - Root
conftest.py— short comment onpytest_plugins.