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 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:

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:

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() and 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() 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() 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) 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.