# Run the Tests

Unlike the other how-to guides, this one assumes a checkout of the repository rather than a running app, because the tests ship in the repo, not in the installed package. See the [Tutorial](../../user-guide/tutorials/index.md) if you don't have a checkout yet.


# Run the suite

The tests live in `tests/` and need `pytest`, which comes with the `dev` extra:

``` bash
pip install -e ".[dev]"    # or: uv pip install -e ".[dev]"
pytest                     # or: uv run pytest
```

`testpaths` is set in `pyproject.toml`, so plain `pytest` from the repo root collects the right directory. The whole suite runs in well under a second: no network, no disk writes, no Shiny session.

Run one file, or one test, the usual way:

``` bash
pytest tests/test_core.py
pytest tests/test_core.py::test_on_this_day_single_match
pytest -k on_this_day
```


# Two different sources of data across the two test files

`tests/test_core.py` never touches the real workout history. It builds a tiny three-record `FIXTURE` list by hand and feeds it straight to [random_workout()](../../reference/random_workout.md#daily_dose.random_workout) and [on_this_day()](../../reference/on_this_day.md#daily_dose.on_this_day), so each test controls exactly what it's asserting against. That means it can force a date with matches in two years, say, and a date with none.

`tests/test_data.py` does the opposite on purpose. It calls [load_workouts()](../../reference/load_workouts.md#daily_dose.load_workouts) with no fixture, loading the real bundled `workouts.json`, then checks properties that should hold for the *actual* 1001-record dataset: exact count, no duplicate dates, every referenced image file present in `www/`. That makes it a regression guard on `scripts/build_workouts_data.py`. If a future edit to the parser silently drops or duplicates entries, this is what catches it.

`tests/test_build_workouts_data.py` tests the parser itself, in isolation, against small in-memory `.Rmd` fixtures written to `tmp_path`. It loads `scripts/build_workouts_data.py` directly with `importlib.util`, since `scripts/` isn't an installed package.


# What the tests cover


## `tests/test_core.py` (5)

Pure logic: picking a random workout and matching on today's date.

| Test | What it pins down |
|----|----|
| `test_random_workout_returns_member` | [random_workout()](../../reference/random_workout.md#daily_dose.random_workout) always returns one of the workouts it was given |
| `test_random_workout_is_seedable` | The same seeded `random.Random` produces the same pick, twice |
| `test_on_this_day_matches_across_years` | Matches on month/day come back across every year present, newest first |
| `test_on_this_day_no_match_returns_empty` | A date with nothing logged returns `[]`, not an error |
| `test_on_this_day_single_match` | A day with one image-kind match still comes back as a one-item list |


## `tests/test_data.py` (4)

The real, bundled dataset: a regression guard on the build script, not on app logic.

| Test | What it pins down |
|----|----|
| `test_load_workouts_count` | The bundled dataset has exactly 1001 workouts |
| `test_every_workout_has_a_valid_date_and_kind` | Every record has an ISO `date` and a `kind` of `"text"` or `"image"` |
| `test_no_duplicate_dates` | No date appears twice, confirming the text-vs-image dedup actually worked |
| `test_image_files_exist` | Every `image_url` the data references exists under the packaged `www/` assets |


## `tests/test_build_workouts_data.py` (2)

The parser, exercised against small `.Rmd` fixtures rather than the real data.

| Test | What it pins down |
|----|----|
| `test_parse_rmd_splits_indented_date_heading` | A date heading nested under the previous entry's list still becomes its own entry, not text merged into the entry above it |
| `test_parse_rmd_raises_on_unparseable_date_heading` | A date-shaped heading `DATE_HEADER_RE` won't match (wrong heading level, trailing text, …) raises `ValueError` instead of being silently dropped into the previous entry |


# What the tests do not cover

The Shiny wiring in `app.py` isn't unit tested. That's the two `@reactive.event` button handlers and the `results` render function. Exercising it needs a live Shiny session, and it's a thin enough layer over `core.py` (see [Explanation](../../user-guide/explanation/index.md)) that there isn't much logic left in it to test in isolation.

Those paths were verified by driving a real browser session with Playwright: clicking **Random Workout** repeatedly until both a text and a photo entry showed up, clicking **On This Day** and confirming it reflected the actual current date, and confirming the empty-state message renders when a date has no match. If you change the button-click wiring, that's the check to repeat, because the suite won't catch it.


# What CI runs on every push

Nothing runs the test suite on push yet. `.github/workflows/docs.yml` builds and deploys the documentation site on every push to `main`, but there's no equivalent workflow running `pytest`, `ruff`, or `mypy`. Running those locally before you push, as above, is what stands in for CI today.
