Skip to main content
Testing, Quality & Craft
🧪 Testing & CraftLesson 13 of 13

Capstone: Messy App → Tested, Clean & Documented

Take a messy app to tested, clean, documented, and reviewable — with a real test suite.

Capstone: Messy App → Tested, Clean & Documented

Stage 3 · Testing, Quality & Craft · Capstone

You vibe-coded something real — it works, users love it, and you moved fast. Now comes the part that separates hobbyists from engineers: you go back in. You add the tests that make you fearless, clean the code that makes you cringe, write the docs that make the next person (future you, a collaborator, an AI pair) feel welcomed instead of lost. This capstone is your before-and-after. By the end, you'll open the repo and feel proud.


🎯 The mission

Take a real application you've already built — ideally something you vibe-coded in a hurry — and bring it up to a professional, shippable standard. You'll measure the gap between where it started and where it ends: lines of test coverage, number of code smells removed, whether a stranger could clone and run it from your README alone. The point isn't to rewrite. It's to lift what already works until it's something you'd show a senior engineer without apology.


🧱 What to do

Work through these layers in order. Each one builds on the last — don't skip ahead, because the tests you write first are what make every cleanup safe.

1. Pick your app and document the starting line

  • Choose a real project: a vibe-coded front end from Stage 1, a backend from D1, a full-stack prototype — anything with logic worth protecting.
  • Take a snapshot of the starting state: no tests, coverage at 0%, a list of 3–5 code smells you already know are there. Be honest; this is your baseline, not a judgment.
  • Identify the critical path — the single sequence of steps a user takes to get the core value of the app (e.g., sign up → create item → see item listed). Everything else is secondary.

2. Set up the test runner and coverage

  • Install and configure a test runner appropriate for your stack (Vitest, Jest, Pytest, Go's built-in testing, etc.).
  • Add a test and test:coverage script to your package.json (or equivalent).
  • Verify you can run npm test from a clean clone and see output. Zero passing tests is fine — the runner must work.
  • Set a coverage threshold in your config (start at 70% for the files you touch; raise it as you go).

3. Write the test suite — critical path first

  • Unit tests for every pure function or isolated module on the critical path. Aim for fast, deterministic, no I/O.
  • At least one integration test that wires two real layers together — e.g., a route handler calling a real database function (use a test database or an in-memory store).
  • At least one end-to-end test that drives the critical path from the outside — a browser test with Playwright/Cypress, or a CLI test that calls your real server.
  • Use test doubles (mocks, stubs, fakes) deliberately: isolate the unit under test; never mock what you own when an integration test would be more meaningful.

A minimal unit test to get the shape right:

// utils/formatPrice.test.js
import { formatPrice } from './formatPrice.js'

describe('formatPrice', () => {
  it('formats whole dollars', () => {
    expect(formatPrice(10)).toBe('$10.00')
  })

  it('rounds to two decimal places', () => {
    expect(formatPrice(9.999)).toBe('$10.00')
  })

  it('returns $0.00 for zero', () => {
    expect(formatPrice(0)).toBe('$0.00')
  })

  it('throws on negative input', () => {
    expect(() => formatPrice(-1)).toThrow('Price cannot be negative')
  })
})

Every assertion should be meaningful — it should fail for a real bug, not just to hit a number.

4. Refactor the worst module — under green tests

  • Pick the single messiest module in the app (the one you dread opening). The tests you just wrote make this safe.
  • Apply a clean code pass:
    • Rename variables and functions to say what they are, not how they work.
    • Extract magic numbers and strings into named constants.
    • Replace nested if chains with guard clauses (return early).
    • Delete commented-out code and dead branches.
    • Break functions longer than ~20 lines into smaller, named pieces.
  • Run the tests after every meaningful change — if they go red, you introduced a bug; fix it before continuing.
  • Write a short "before / after" comparison: paste the old function and the new one side by side. This becomes a deliverable.

5. Write / upgrade the README

Your README must let a complete stranger clone, configure, run, test, and deploy the app without asking you a single question. At minimum:

## Prerequisites
Node 20+, a Supabase project (or local Docker), an .env file.
Copy `.env.example` and fill in the values.

## Install
npm install

## Run locally
npm run dev        # http://localhost:3000

## Test
npm test           # unit + integration
npm run test:e2e   # end-to-end (requires dev server running)

## Deploy
npm run build && npx vercel deploy --prod

That skeleton is the floor, not the ceiling. Add a description, a screenshot, environment variable docs, and a link to the live demo.

6. Write one Architecture Decision Record (ADR)

  • Pick one non-obvious decision in the app — why you chose a particular library, how you structured authentication, why you split the data model the way you did.
  • Write a one-page ADR: Context → Decision → Consequences. Store it at docs/adr/001-<short-slug>.md.
  • ADRs are how future-you (and future collaborators) avoid relitigating decisions that were already made carefully.

7. Clean up git history and adopt a PR flow

  • Ensure every commit from here forward follows Conventional Commits format: feat:, fix:, test:, refactor:, docs:.
  • Create a branch for at least one of the changes you made above (git checkout -b refactor/format-price-cleanup).
  • Open a pull request (even if it's just to your own main). Write a PR description that explains why, not just what.
  • Review your own PR as if you were a stranger — would you merge this? Would the CI pass?

8. Self code-review against the Code Review Rubric

  • Go through the Code Review Rubric from Lesson 9 line by line, treating your entire diff as the thing under review.
  • Write a 1–2 page self-review: what passed, what you caught and fixed, what you deliberately left for a future ticket and why.
  • This is not busywork — it's the habit that turns "it works" into "it's maintainable."

🗺️ Run it through B.U.I.L.D.

LetterPhaseWhat it means for this capstone
BBlueprintThe critical path you mapped; the baseline you documented
UUnderstand & CleanThe refactor pass — you can't clean safely without the tests
IImplement & IntegrateThe integration and e2e tests that prove the layers fit together
LLaunchThe README that makes it launchable by anyone, not just you
DDocument, Test, DeployCoverage report, ADR, self-review, PR — the whole D column

The key insight: D and U are load-bearing here. The tests (D) are what make U (clean) safe. Every other step flows from that. Don't skip the order.


🧪 Deliverables

Submit all of the following — these are concrete artifacts, not vibes:

  1. A passing test suite with a coverage report — screenshot or CI output showing the percentage. The critical path must be covered. Flakey tests don't count; fix them or delete them.

  2. A before/after refactor document — the old function and the new function side by side, with a 2–3 sentence explanation of each change and why it matters.

  3. A real README — passes the "stranger test": someone who has never seen your project clones it, follows your README, and has a running dev server within 10 minutes.

  4. One ADR — a genuine Architecture Decision Record for a real decision in the app. Must include Context, Decision, and Consequences.

  5. A self-review write-up — you ran the Code Review Rubric against your own diff and wrote up the results. At least one thing you caught and fixed; at least one thing you intentionally deferred with a reason.

  6. A measurable improvement — compare your baseline to your final state on at least two of these axes: lines covered, number of named smells removed, README completeness, commit message quality. Numbers beat adjectives.


🏆 Stretch goals

These are not required, but they foreshadow D6 and separate good work from great work:

  • Add CI — set up a GitHub Actions workflow that runs npm test and npm run test:coverage on every push and every pull request. A red test blocks the merge. This is what D6 (DevOps) is built on.
  • Get a real review — share your PR with a peer, a mentor, or an AI and ask for a formal code review. Respond to every comment, even if your response is "won't fix — here's why."
  • Add a pre-commit hook — use Husky or a simple .git/hooks/pre-commit script that runs the linter and the unit tests before any commit is allowed. Fail fast, locally.
  • Achieve 90% coverage on the critical path module — not 90% overall (that's a trap), but 90% on the single most important file in the app.
  • Write a second ADR — document a decision you made during this capstone (e.g., "why I chose Vitest over Jest").

✅ You're done when…

  • The Code Review Rubric self-review is written and filed — you went line by line, you caught real things, and you documented what you fixed and what you deferred.
  • The Pre-Ship Checklist is checked — tests passing, README accurate, no hardcoded secrets, coverage above threshold, conventional commits from here forward.
  • The critical path is covered by meaningful tests — unit + integration + e2e all pass, assertions would catch real regressions, not just presence checks.
  • The README lets a stranger run, test, and deploy the app — you tested this by following your own instructions from a fresh directory (or asked someone else to).
  • The before/after refactor is documented — not just done, but explained: what changed, why, and what the tests proved stayed the same.
  • The ADR exists and is honest — it captures a real decision, not an obvious one, and the Consequences section is candid about trade-offs.

A note on vibe coding: You got here because you could build fast. That skill is real and it matters — don't let anyone tell you otherwise. This capstone isn't about slowing down. It's about building a foundation that lets you go faster next time — because the tests catch the bugs before they catch you, the clean code is readable at 2 AM, and the README means your team (or your future self) never has to reverse-engineer what you built. The best engineers vibe-code the prototype and bring it to this standard. Now you do both.


➡️ Next: Stage 3 finishes with DevOps, Deployment & Operations — ship it and keep it alive. Build It Right, Or Don't Build It At All. 🏛️

Always-on rigor toolkit

🏛️ Build It Right, Or Don't Build It At All.