Capstone Brief
3 minTake an existing app and bring it up to professional quality:
- A test suite: unit + at least one integration test, ~90% coverage.
- Linting (ruff) and formatting clean.
- Type hints + mypy clean on the core modules.
- Pre-commit hooks + a green GitHub Actions CI.
Pick Your App
5 minGood candidates from earlier levels:
- The L4 blog or library management system (CRUD + Flask).
- The L3 Dungeon Quest (OOP RPG).
- The L2 Tic-Tac-Toe or Wordle.
- Any project you wrote that has zero or few tests.
Real testing work is usually retrofitting tests onto existing code — and that code often needs small refactors (extract logic from I/O, inject dependencies) to become testable. That refactoring, guided by the goal of testability, is itself the lesson.
The Plan · A Quality Retrofit
14 min1. MEASURE run coverage to see the starting point (probably ~0%) 2. STRUCTURE tests/ folder, pyproject.toml config, app factory if needed 3. UNIT test the core logic (most of your tests live here) 4. INTEGRATION test the seams (repo+DB, routes via test client) 5. GAPS use coverage --cov-report=term-missing to chase to ~90% 6. STATIC ruff + ruff format + mypy clean 7. AUTOMATE pre-commit hooks + GitHub Actions CI 8. DOCUMENT README: how to run tests, coverage badge
Make untestable code testable first
# before: logic tangled with input/output — hard to test def play(): name = input("name? ") score = compute(name) print(f"{name}: {score}") # after: pure logic extracted — easy to test; I/O is a thin shell def compute(name): ... # ← test THIS directly def play(): name = input("name? ") print(f"{name}: {compute(name)}")
Chase coverage to the gaps, with real assertions
pytest --cov=myapp --cov-report=term-missing # read the Missing column → write a meaningful test per uncovered line # (no coverage theatre — Lesson 27/29)
The full pyproject.toml
[tool.pytest.ini_options] testpaths = ["tests"] addopts = "--cov=myapp --cov-branch --cov-report=term-missing --cov-fail-under=90" [tool.ruff.lint] select = ["E", "F", "I", "B"] [tool.mypy] python_version = "3.12"
Worked Sketch · Retrofitting the L4 Blog
12 minblog/ ├─ blog/ │ ├─ __init__.py # create_app() factory (refactored for testing) │ ├─ db.py # repo functions │ └─ routes.py # Flask routes ├─ tests/ │ ├─ conftest.py # client + in-memory db fixtures │ ├─ test_db.py # UNIT: repo functions (real :memory: DB) │ └─ test_routes.py # INTEGRATION: routes via test client ├─ .pre-commit-config.yaml ├─ .github/workflows/ci.yml └─ pyproject.toml
# tests/test_db.py — unit tests of the data layer def test_add_and_get(blog): pid = blog.add_post("Hi", "Body text here") assert blog.get_post(pid)["title"] == "Hi" def test_search(blog): blog.add_post("Python tips", "...") blog.add_post("Cooking", "...") assert len(blog.search("Python")) == 1 # tests/test_routes.py — integration via the test client def test_create_via_route(client): resp = client.post("/new", data={"title": "First", "body": "Hello there"}, follow_redirects=True) assert b"First" in resp.data
# the progression you're aiming for: $ pytest --cov=blog start: blog.py 0% (no tests) ...add unit tests... 65% ...add route tests... 88% ...chase Missing lines... 91% ✅ above the 90% gate ruff: All checks passed mypy: Success: no issues found CI: ✅ green
Read the diff
The retrofit follows the plan: refactor for testability (app factory), unit-test the data layer against an in-memory DB, integration-test the routes via the test client, then chase the coverage gaps to 91%. Layer on ruff/mypy/pre-commit/CI and the once-untested blog is now a professional, maintainable codebase. That's the whole of Level 6 applied at once.
Do the Retrofit
13 minWork the 8-step plan on your chosen app. Tips:
- Start by measuring — know your baseline coverage.
- Refactor only as much as needed to make code testable.
- Most tests are unit; add a few integration tests for the seams.
- Use
--cov-report=term-missingto find the last gaps. - Don't pad with assertion-free tests — 88% honest beats 95% theatre.
Stretch Goals
8 min- Add one E2E test (Playwright) for the app's most important journey.
- Find and fix a real bug your new tests exposed; commit the regression test.
- Add branch coverage and push to 90% branch (not just line) coverage.
- Add the CI matrix across two Python versions.
Recap
3 minYou retrofitted a real app to professional quality: refactor for testability → unit + integration tests → chase coverage to ~90% with honest assertions → ruff/format/mypy clean → pre-commit + CI green. This is exactly what a software engineer does on day one of a real job. Next: the PCET exam prep that closes the level.
Deliverable
4 min- A GitHub repo with a
tests/suite (unit + ≥1 integration) at ~90% coverage. pyproject.tomlconfiguring pytest+cov, ruff, mypy..pre-commit-config.yamland a passing.github/workflows/ci.yml.- A README with run instructions and a green CI badge.
- A note on any real bug your tests caught during the retrofit.
Capstone rubric (10 pts) test suite runs & passes 2 ~90% coverage with real assertions 2 at least one integration test 1 ruff + format clean 1 mypy clean on core modules 1 pre-commit config works 1 green CI workflow 1 README + caught-bug note 1
The most-valued item: a documented real bug your tests caught. Tests that find bugs are tests that earned their keep.