DEV Community

Daniel Pertu
Daniel Pertu

Posted on

140 tests on our website, and every single one is about money or search engines

Notifio is a desktop app that watches rental search pages and emails you when a new listing appears. The app and the website are two codebases in one repository, and they have two very different relationships with testing. The desktop side has no test framework at all, just three scripts and a rule about imports. The website has a real suite, and I had never counted what it covers.

The suite, measured

$ node --test
ℹ tests 140
ℹ suites 34
ℹ pass 140
ℹ fail 0
ℹ duration_ms 1865
Enter fullscreen mode Exit fullscreen mode

Five files, 1,388 lines, 257 assertions. No Jest, no Vitest, no mocking library, no DOM, no database, no network. The runner is Node's own, which reads the TypeScript directly and charges one price for it: the @/ path alias does not exist, so every test imports with a relative path and a .ts extension.

file                               it()  cases  lines
pricing/display-currency.test.ts     42     46    461
seo/indexnow.test.ts                 31     31    292
stripe/managed-payments.test.ts      23     23    222
stripe/checkout-session.test.ts      14     18    255
seo/metadata-length.test.ts           4     22    158
Enter fullscreen mode Exit fullscreen mode

114 it() calls produce 140 tests, because some of them are written inside loops over a data table and each row becomes its own case. That is the only indirection in the whole suite.

What the suite can reach

Every import in every test file, deduplicated, is eight source modules:

src/lib/pricing/display-currency.ts
src/lib/pricing/constants.ts
src/lib/stripe/checkout-session.ts
src/lib/stripe/managed-payments.ts
src/lib/seo/indexnow.ts
src/lib/compare/targets.ts
src/lib/articles/audiences.ts
src/lib/articles/guides.ts
Enter fullscreen mode Exit fullscreen mode

Eight of the 79 TypeScript files under src. 2,271 lines of 12,007. The last three are data rather than code, so the number of actual functions under test is smaller than it already looks.

Put the subject matter next to the file list and the shape is unmistakable. Five files: what currency to quote a visitor, how to build a Stripe line item, when a sale is misrouted for tax, what to tell Bing, and whether a title fits in a search result. The website has 140 tests and not one of them is about rental listings.

That is deliberate, and here is the rule it follows

The suite covers the failures that are silent.

If the currency logic picks wrong, the desktop app shows a price the server told it to show and the buyer is charged something else. Nobody files that bug; they just do not buy. If a title drifts two characters over the budget, Google truncates it and the page quietly earns less. If the IndexNow submitter posts a URL that does not exist, the response is still a 200. Every one of those is a problem you find in a dashboard, months later, with no way to tell when it started.

The untested half of the codebase fails loudly instead. The eight route handlers are:

/api/validate            licence check from the desktop app
/api/notify              the alert email the app asks us to send
/api/checkout            buy a licence
/api/checkout/upgrade    buy the auto-reply upgrade
/api/contact             the form on /help
/api/download/[platform] a 302 to a signed installer URL
/api/webhooks/stripe     issue a licence after payment
/api/webhooks/resend     forward inbound mail
Enter fullscreen mode Exit fullscreen mode

If activation breaks, the next person who buys writes to support within the hour. If the alert email stops, somebody whose rental search went quiet tells us the same day. A test suite is one way to learn about a bug and an annoyed customer is another, and for most of that list the customer is faster and more accurate.

The one place that argument falls down

The Stripe webhook. Its failure mode is a person who paid and has no licence, which is loud, but it is also the worst hour of that person's day and it is the most intricate code in the repository:

} catch (err) {
  // Concurrent webhook won the race and inserted first. Fall back to
  // the row that won so we still deliver a valid token.
  const [row] = await db
    .select()
    .from(licenses)
    .where(eq(licenses.email, email))
    .limit(1);
  if (!row) throw err; // genuine DB error → 500 so Stripe retries
  token = row.token;
Enter fullscreen mode Exit fullscreen mode

Three branches before that one: this exact session already processed, a different session for an email that already has a licence, and a fresh insert. All four paths have to end with the same token reaching the same inbox, because the token is the product. Not one of them is exercised by anything but a real payment.

It needs no database to test. Pass in a fake db with four scripted behaviours and assert the token that comes out. That is the next test file, and it is the only gap on the list that makes me uncomfortable.

The test that checks data instead of code

metadata-length.test.ts is four it() calls in the source, 22 cases at runtime, and the most valuable file of the five. Every page on the site must have a title under 60 characters and a description under 160, and the rendered title is what counts:

/** The suffix the root layout's title template appends to every child page. */
function titleTemplateSuffix(): string {
  const layout = readFileSync(LAYOUT, "utf8");
  const match = layout.match(/template:\s*"(.*?)"/);
  assert.ok(match, "root layout no longer declares a title template");
Enter fullscreen mode Exit fullscreen mode

It reads the root layout with a regular expression to find the | Notifio suffix, rather than trusting anybody to remember that ten of the sixty characters are already spent. Hand-counting is what let three titles drift to exactly 60 before this existed.

It also refuses to be given a list of what to check:

/**
 * The alerts barrel uses extensionless imports that Node cannot resolve, so the
 * site files are loaded straight from the directory. Globbing rather than
 * listing them also means a new market file is covered the day it is added.
 */
Enter fullscreen mode Exit fullscreen mode

A limitation of the runner turned into the better design. Those four calls cover all 15 site pages, the five comparisons, the five guides and the three audience pages, and they will cover the sixteenth site page without an edit.

What I would change

Nothing about the ratio. A 140-test suite that runs in under two seconds with no dependencies gets run, and a suite that needs a database does not. The two things I am taking from the census are the webhook file above, and the fact that these tests only ever run when I type node --test: there is no CI in this repository at all, which I have learned the hard way on a sister project is how you find out two of them are red.

The product the tests are protecting is at notifio.app: a one-time licence rather than a subscription, installers for Windows and both Mac architectures, the per-site setup guides at notifio.app/alerts, and the questions the contact form exists for at notifio.app/help.

Top comments (0)