All writing
EngineeringSeptember 24, 20264 min read

The First Tests in an Untested Codebase: Start With the Code That Decides

How I introduced Vitest to two production Next.js sites with zero tests, what I covered first, and how I checked that the tests actually catch the bugs they claim to.

Henry Iddirisu

Henry Iddirisu

AI product engineer

A test runner turning a column of modules from grey to green

Zero tests is a starting position, not a verdict.

Two production Next.js sites I work on, a main website and a landing-page app, had no test infrastructure at all. Both were full of logic that decided things for real users: which phone dial code a lead form submits, which locale a visitor gets, which cookies drive marketing attribution.

Here's how I added the first suites, and the check I now run on any new test.

1. Set up the boring parts once

Vitest, jsdom, and Testing Library, with the same @/ path alias the app uses, a jest-dom setup file, and three scripts: test, test:run, and coverage. Coverage was scoped to src/lib on purpose. The first goal was to protect decisions, not to chase a percentage across UI components.

2. Test the code that decides, not the code that renders

The first suite on the landing app was 63 tests across seven modules that had never been tested:

  • Geolocation: the full visitor-country chain, including header parsing, the paid-API fallback, and a test that a Cloudflare hit never calls the paid API.
  • Phone numbers: validation and cleanup rules for what the forms submit.
  • Locales: mapping pt-BR and pt-PT down to the right language.
  • Trustpilot: brand-by-hostname summaries, and fallbacks when the data is junk or the service is offline.
  • Attribution: the cookie allowlist and which URL parameters win.

On the main website, the next pass added 98 tests for SEO metadata (canonical URLs, hreflang sets per brand with x-default, OpenGraph and Twitter cards, robots rules) and image optimization (preload rules, srcset generation, lazy vs. eager loading).

These are pure functions with real consequences. A wrong hreflang or canonical doesn't crash anything. It quietly costs search traffic, and a test is the only thing that notices.

3. Add integrity checks for data tables

Some of the most valuable tests assert nothing about behaviour, only completeness. The SEO keyword tables fall back to English for every language that lacks its own entry, so a test checks that the English set is complete for both brands. If someone deletes a key, the fallback silently breaks, and now a test fails first.

4. Prove the test against history

Months later, the landing app's tests broke on master: 13 failures, all "is not a function". The suite had landed on the staging branch while a separate change on master had deliberately deleted three phone helpers. The tests were importing functions that were meant to be gone.

I rewrote that section to test the function that replaced them, composePhoneNumber, asserting the full international string the forms submit: digits never truncated, the leading trunk 0 dropped, a 00 prefix handled, a doubled dial code removed exactly once.

Then I checked the new tests against the old code:

  • the version from the commit that deleted the helpers fails 17 of them
  • an earlier Spain-only fix fails 14
  • removing any one guard (the length gate, the leading-zero rule, the 00 rule, or the single-strip) fails at least one

A test that has never failed has never proved anything. Checking it against the bugs it's meant to catch takes a few minutes, and afterwards you can trust it.

What I'd tell anyone starting from zero

  1. Scope the first suite to pure logic in lib/. It's fast, stable, and where the decisions are.
  2. Add integrity tests for any table that other code depends on being complete.
  3. When a test is written for a bug, run it against the buggy version and make sure it fails.
  4. Keep the suite on every branch that ships. Tests that live on only one branch drift.
#testing#vitest#nextjs#typescript
Henry Iddirisu

Henry Iddirisu

AI product engineer · Accra, Ghana · Remote

Keep reading