Skip to content
Development

astro-blog finally got tests, and they found a real bug

By Victor Da Luz
astrotestingtypescriptdev-logastro-tools

@vdaluz/astro-blog is one of four small Astro component packages I maintain for my sites. Two of the other three already had node --test suites. astro-blog had zero, which was starting to bug me every time I touched the scoring logic in RelatedPosts or the JSON-LD builder with no way to know if I’d broken something.

So I sat down to fix that: five pure-function modules, five test files, following the same convention the sibling package already used. node:test, node:assert/strict, importing straight from the TypeScript source with no build step in between.

The devDependency question

One of those modules imports astro/zod to build a content-collection schema. That’s a subpath export of the astro package itself, and this repo ships zero runtime dependencies on purpose - it’s raw source, consumed by whatever Astro version the calling site already has installed. Running a test file that imports that module outside an actual Astro app meant Node couldn’t resolve the import at all.

The fix was adding astro as a devDependency, test-only. It never reaches consumers, since npm doesn’t install a package’s devDependencies when you depend on it. But it needed spelling out explicitly before I touched it, because “dependency-free” is a real constraint on this repo, not just a description of it.

The date-formatting trap

The i18n module has a formatDate function, and every naive way of writing a fixture for it is a trap. Build the test date from an ISO string, parse it as UTC midnight, then format it in whatever timezone the test happens to run in, and you can be off by a day depending on where the box sits relative to UTC.

I build fixture dates from local components instead of ISO strings for the default-locale path, and pin the options-argument path to a fixed timeZone explicitly so it can’t drift. I also stopped asserting an exact string for the Spanish locale output, since ICU formatting for a bare language tag without a region has shifted across Node versions before. That test only checks for the substrings that actually matter.

Ran the whole suite under two wildly different system timezones, UTC and UTC+14, before trusting it. Both clean.

The bug the tests actually caught

One of the modules imports a sibling module without a file extension: from './relatedPosts' rather than from './relatedPosts.ts'. Vite resolves that fine, so it’s worked in every consuming site without a hitch. Plain Node ESM resolution does not. It throws ERR_MODULE_NOT_FOUND, full stop, no fallback.

Nobody had ever run this file outside a bundler before. The moment I did, which was the whole point of adding tests, it broke immediately. One-line fix, but it’s exactly the kind of thing “it works in the app” papers over forever.

Lesson

The bug wasn’t in the logic I was testing. It was in the plumbing that lets the logic run at all outside its usual bundler. That’s a decent argument for testing library code with the plainest possible runner rather than reaching for whatever tooling the consuming app happens to use. The plain runner has fewer opinions, and fewer opinions means fewer things quietly compensating on your behalf.

Related reading