A scalable test layout that mirrors the production code, so a contributor can find (or add) tests by guessing the same path as the source file.
backend/
├── pytest.ini
├── requirements-dev.txt
└── tests/
├── conftest.py # ← shared fixtures (DB, async_client, fake_llm, sample resumes)
├── fixtures/ # ← reusable test data: resumes, file bytes, fake LLM
│ ├── resumes.py
│ ├── llm_fakes.py
│ └── files.py
├── unit/ # ← pure-function tests (no FastAPI, no DB)
│ ├── test_config.py
│ ├── test_auth.py
│ ├── test_runtime_settings.py
│ ├── test_schemas.py
│ ├── test_llm_factory.py
│ ├── test_graph_helpers.py
│ ├── test_routing.py
│ ├── test_models.py
│ ├── test_section_preferences.py
│ └── test_documents_normalize.py
├── api/ # ← HTTP-level tests against the live FastAPI app
│ ├── test_health_config.py
│ ├── test_sessions.py
│ ├── test_messages.py
│ ├── test_memory.py
│ ├── test_document_routes.py
│ ├── test_settings_routes.py
│ ├── test_metrics_routes.py
│ ├── test_upload_routes.py
│ └── test_auth_flow.py
├── agents/ # ← LangGraph nodes + end-to-end pipeline (LLM faked)
│ ├── conftest.py # ← shared `base_state` fixture
│ ├── test_intent_guard.py
│ ├── test_parser_and_jd.py
│ ├── test_generator_reviewer_enhancer.py
│ ├── test_cover_letter_and_outreach.py
│ └── test_full_pipeline.py
└── documents/ # ← PDF / DOCX / ODT generator smoke tests
├── test_pdf_generator.py
├── test_docx_generator.py
├── test_odt_generator.py
└── test_cover_letter_pdf.py
cd backend
pip install -r requirements-dev.txt
# Everything (~30s on a modern laptop, fully offline)
pytest
# One layer
pytest tests/unit
pytest tests/api
pytest tests/agents
pytest tests/documents
# By marker
pytest -m unit
pytest -m api
pytest -m agents
pytest -m documents
pytest -m "not slow"
# Coverage
pytest --cov=app --cov-report=term-missingdocker compose --profile test up --build --abort-on-container-exitRuns the full suite inside the same Python 3.13 base image the app ships on, so a green local run guarantees a green CI run.
-
Find the corresponding source file (
app/api/foo.py). -
Open or create the matching test file under the same sub-tree (
tests/api/test_foo.py). -
Use the existing fixtures from
conftest.py— don't roll your own client or DB session:Fixture What you get async_clienthttpx.AsyncClientbound to the live FastAPI app (no port, no network)db_sessionAn AsyncSessionyou can use to seed/inspect rows directlyfake_llmPatches every get_llm(...)call so agents run offline. Returns aFakeLLMyou can drive with.set(agent_key, payload)sample_resumeFully-populated resume dict (every section the renderers care about) minimal_resumeSmallest valid resume — use to test degrade-gracefully behaviour jd_textA short, realistic job description string base_state(agents/ only) A fully-formed AgentStateyou can mutate per test -
Database is wiped before every test via the autouse
_reset_databasefixture inconftest.py. No cross-test bleed; no manual cleanup.
The fake is fingerprinted from the system prompt so each agent gets its own payload. Override per-agent:
def test_my_thing(fake_llm, base_state):
fake_llm.set("reviewer", {"overall_score": 50, "critical_issues": ["x"]})
fake_llm.set_raw("parser", "not even json — should fall back gracefully")
fake_llm.fail_with("generator", RuntimeError("provider down"))
...Available agent keys: intent, parser, jd_analyzer, generator,
reviewer, enhancer, cover_letter, outreach.
- Async tests are auto-detected (
asyncio_mode = "auto"inpytest.ini); no@pytest.mark.asynciodecorator needed. - Module-level
pytestmarksets the marker for every test in the file — use it instead of decorating each function. - Polling: API tests that wait for the background pipeline use
_poll_until_done(seetest_messages.py). 10-second cap. - No real provider keys ever land in the test environment —
conftest.pyscrubs them on import.
The GitHub Actions workflow at .github/workflows/ci.yml runs the suite on
Python 3.13 against every push and PR, uploads the coverage XML,
and (on pushes to main / v* tags) publishes the backend image to Docker
Hub.