Back to path
AdvancedInvoiceDesk · Project 10 of 12 ~7h· 5 milestones

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.

Vitest table-driven unit testingIntegration testing against real PostgresPlaywright end-to-end automationMocking Stripe webhook eventsGitHub Actions service containersTest data isolation and cleanupFlake-proofing async assertionsTest pyramid design

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:

tests/unit/money.test.tsts
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. 1

    Unit test the money math

    5 guided steps

    Every 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. 2

    Integration tests on a real Postgres

    6 guided steps

    Server 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. 3

    E2E the money path with Playwright

    5 guided steps

    This 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. 4

    Wire it into CI

    5 guided steps

    A 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. 5

    Flake-proof it

    5 guided steps

    A 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

4 starter files, ready to clone
5 guided milestones
5 full reference solutions
9 code blocks explained line-by-line
5 "is it working?" checks
6 interview questions it prepares you for

You'll walk away with

tests/unit/money.test.ts with 100% branch coverage on lib/invoice-math.ts
tests/integration/invoices.actions.test.ts passing against a real, migrated Postgres test database
tests/e2e/checkout.spec.ts covering signup through a Paid invoice with a mocked Stripe webhook
.github/workflows/ci.yml running the full suite with postgres and redis service containers on every PR
One documented flaky test found and fixed, with the before/after diff
Zero flaky runs across 5 consecutive local repeats of the e2e suite with retries set to 0

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