Chapter 19: Testing your application
1. Why testing matters
You've built a Flask app on top of SQLite: charts, playlists, OAuth, settings actions. Everything works when you click through it. This chapter covers the gap between "works when I demo it" and "I can prove it still works after the next change". The toolkit is pytest, in-memory SQLite fixtures, mocked Spotify calls, and Flask test client coverage of the routes Chapter 18 built.
Three chapters of code now sit on disk: Chapter 16's per-feature scripts, Chapter 17's Flask spine, Chapter 18's three feature pages. Every change you make from here on can break something. A new playlist source might silently regress the existing ones. A schema tweak might corrupt the catalogue-growth query. An OAuth refactor might break the disconnect flow's session-key cleanup. Without tests, you find out when a recruiter or hiring manager clicks something during a demo. With tests, you find out in the seconds before you commit.
This chapter is the one that turns a portfolio project into something you can put on a resume. "Tested with pytest, nearly 50 tests, runs in under five seconds, GitHub Actions on every push" is a different kind of artifact than a working app. It signals that you understand maintainability, not just feature completion -- and that's what the next round of code review (interview, code review at a new job) will pressure-test you on.
- Set up pytest with a clean project structure and shared fixtures in
conftest.py - Apply the Arrange-Act-Assert pattern to pure functions like
_build_playlist_queryandnormalise_track - Mock Spotify API calls with
unittest.mockandpytest-mockso tests never depend on the network - Write integration tests against an in-memory SQLite database seeded with realistic snapshot fixtures
- Use Flask's
app.test_client()to exercise the eight routes Chapter 18 built, including AJAX, CSRF, and authenticated session manipulation - Freeze time with
freezegunto make date-dependent tests deterministic - Measure coverage with
pytest-covand read reports without obsessing over the percentage - Run the suite on every push with GitHub Actions
tests/conftest.py— shared fixtures:in_memory_db,seeded_db,app_client,authenticated_clienttests/test_helpers.py— unit tests for_format_month,_build_playlist_query,normalise_track,retry_with_backofftests/test_database.py— integration tests forcalculate_taste_stats,get_taste_chart_data,find_forgotten_gemstests/test_spotify_client.py— mocked tests forSpotifyClient's OAuth methodstests/test_routes.py— Flask test client coverage for/analytics,/playlists,/api/generate-playlist, and the five/settings/*routestests/test_auth.py—@require_authdecorator and OAuth state CSRF defencetests/test_features.py— Chapter 16's monthly-snapshot writes (creation and within-day idempotency) with frozen timepytest.ini+.gitignore+.github/workflows/tests.yml— configuration + CI
Carry-forward from earlier chapters
The application code stays exactly where Chapters 16, 17, and 18 left it. You're not refactoring; you're adding a tests/ folder alongside the existing files. Every module you've built becomes a test target:
- Chapter 16 already showed you the basics. Its testing page introduced in-memory SQLite fixtures (
:memory:) andunittest.mock.MagicMock / patchfor the Spotipy client. This chapter grows that into a full pytest suite with shared fixtures, parameterised tests, and CI integration. - Chapter 17's helpers are unit-test gold.
_format_month,get_db_connection,calculate_taste_stats,refresh_token_if_needed, therequire_authdecorator -- all pure-or-near-pure, all easy to test against in-memory fixtures. - Chapter 18 hands you a route inventory. Eight Flask routes (Analytics, Playlist Manager form + AJAX, five Settings actions) with explicit edge cases: silent-coerce on invalid
?range=, playlist-source whitelist validation, two-step confirmation on destructive actions, X-CSRFToken header for AJAX. Chapter 18's review page commits Chapter 19 to pressure-test all of these.
The testing pyramid
Not every test costs the same. The testing pyramid is the standard heuristic for where to spend effort: lots of fast unit tests at the base, fewer integration tests in the middle, and very few end-to-end tests at the top. Each layer catches different problems with different costs.
- Unit tests run in milliseconds. They verify pure functions in isolation -- no database, no network, no Flask app. The Music Time Machine has plenty of these:
_format_month,_build_playlist_query,normalise_track,retry_with_backoff. When a unit test fails, the cause is usually obvious from the test name. - Integration tests run in tens of milliseconds. They verify components working together against real (but ephemeral) dependencies: in-memory SQLite for database queries, Flask's test client for routes. Slower than unit tests, but they catch SQL mistakes, schema assumptions, and request/response bugs that unit tests can't.
- End-to-end tests run in seconds and break easily. They simulate full user workflows across OAuth, Spotify, and the database. Powerful but expensive: real API access, real network, brittle. This chapter keeps them minimal -- a quick manual smoke test for "does the whole app still work?" rather than automated coverage.
The chapter focuses on the highest-return layers: unit and integration. Six of the eight sub-pages live there. The flask-routes sub-page is the integration-test centerpiece because Chapter 18's outbound commitment lands there. End-to-end stays out of scope.
How the chapter unfolds
You'll start by setting up pytest with a project structure that mirrors what Chapters 16-18 actually shipped (single-file app.py plus per-feature Chapter 16 scripts). Then unit tests on the pure-function helpers. Then mocking the Spotify boundary so tests don't need network access. Then in-memory SQLite for database queries. Then Flask's test client for the routes Chapter 18 built. Then time-dependent logic and coverage measurement. The review page locks in what carries forward to Chapter 20's deployment.
By the end, you'll have a suite that runs on every push, catches regressions before they reach production, and gives you a real answer to "how do you know your code works?"