Build a test suite that catches regressions
Continues from the last build: Hot reads are cached with correct invalidation and abusive traffic gets 429s.
Nine rungs in, it does a lot. Invoices total correctly, org scoping and RBAC hold, Stripe checkout and webhooks move real money, background jobs render PDFs, uploads are presigned and private, lists paginate on a real cursor, and hot reads are cached with rate limiting on top.
What you'll build
It now has a real test pyramid instead of manual clicking. Dozens of table-driven unit tests pin down cents-level rounding and status transitions in milliseconds. A layer of integration tests exercises the actual createInvoice server action against a real, migrated, truncated-between-tests Postgres database, proving org scoping and RBAC denials hold at the data layer. One thick Playwright test walks signup through a Stripe test-card checkout and a mocked webhook to a Paid invoice, the one flow that must never break silently again. GitHub Actions runs all of it, with real postgres and redis service containers, on every pull request, and fails the PR red if any layer breaks. A deliberately flaky test was found and fixed, and the suite now passes ten times in a row locally with zero retries.
See how we teach, before you sign up
You don't just get code dumped on you. Every starter file and every solution is explained line-by-line, in plain English. Here's one real file from this project:
import { describe, it, expect } from 'vitest';
import { computeLineTotalCents, applyTaxCents, computeInvoiceTotalCents, canTransitionStatus } from '@/lib/invoice-math';
describe('computeLineTotalCents', () => {
it.each([
{ qty: 3, unitPriceCents: 333, expected: 999 },
{ qty: 7, unitPriceCents: 1, expected: 7 },
{ qty: 1, unitPriceCents: 999999, expected: 999999 },
])('$qty x $unitPriceCents cents = $expected cents', ({ qty, unitPriceCents, expected }) => {
expect(computeLineTotalCents(qty, unitPriceCents)).toBe(expected);
});
});
describe('applyTaxCents', () => {
it.each([
{ subtotalCents: 10000, taxRateBps: 825, expected: 825 },
{ subtotalCents: 333, taxRateBps: 1000, expected: 33 },
{ subtotalCents: 1, taxRateBps: 50, expected: 0 },
])('rounds $subtotalCents cents at $taxRateBps bps to $expected cents', ({ subtotalCents, taxRateBps, expected }) => {
expect(applyTaxCents(subtotalCents, taxRateBps)).toBe(expected);
});
});
describe('computeInvoiceTotalCents', () => {
it('sums line items and adds tax without floating point drift', () => {
const items = [
{ qty: 3, unitPriceCents: 333 },
{ qty: 2, unitPriceCents: 150 },
];
expect(computeInvoiceTotalCents(items, 825)).toBe(1406);
});
});
describe('canTransitionStatus', () => {
it.each([
{ from: 'draft', to: 'sent', allowed: true },
{ from: 'sent', to: 'paid', allowed: true },
{ from: 'sent', to: 'draft', allowed: false },
{ from: 'paid', to: 'sent', allowed: false },
{ from: 'draft', to: 'paid', allowed: false },
])('$from -> $to allowed: $allowed', ({ from, to, allowed }) => {
expect(canTransitionStatus(from, to)).toBe(allowed);
});
});
Reading this file
expect(computeLineTotalCents(qty, unitPriceCents)).toBe(expected)Table-driven cases assert integer cents, never floats, so rounding bugs cannot hide behind a tolerance.{ subtotalCents: 333, taxRateBps: 1000, expected: 33 }The case that actually exercises rounding, 333 cents at 10 percent is 33.3 cents and must round to a whole cent.expect(computeInvoiceTotalCents(items, 825)).toBe(1406)Proves tax is computed on the summed subtotal, not per line item.{ from: 'paid', to: 'sent', allowed: false }Invalid transitions are tested both directions, so a bug that always returns true still fails.
Starter file for this rung.
That's 1 of 9 explained code blocks in this single project.
The build, milestone by milestone
- 1
Unit test the money math
5 guided stepsEvery dollar figure in the app flows through these functions. If tax rounding drifts by one cent on thousands of invoices, finance notices before engineering does.
- 2
Integration tests on a real Postgres
6 guided stepsServer actions are where org scoping and RBAC actually live. Testing them against a real Postgres catches bugs unit tests structurally cannot see, like a WHERE clause missing an orgId filter.
- 3
E2E the money path with Playwright
5 guided stepsThis is the one flow that must never silently break: a client pays and the invoice does not flip to paid. One thick e2e test on this exact path is worth more than ten shallow ones.
- 4
Wire it into CI
5 guided stepsA test suite nobody runs is a test suite that rots. Wiring it into CI makes all tests pass a fact about the pull request, not a claim about someone's laptop.
- 5
Flake-proof it
5 guided stepsA flaky test suite trains everyone to ignore red builds. Fixing the underlying timing and data-isolation bugs is what keeps CI trustworthy.
What's inside when you start
You'll walk away with
This is portfolio-grade. Build it free.
Sign up to unlock every milestone step-by-step, the code skeletons, full reference solutions, and checkable tasks, with your progress saved as you build.
Start building