Writing tests that check the right thing
Closed out a small backlog item today: two files with real coverage gaps from an earlier audit. One job had never been tested at all, and one HTTP client had zero tests. Not exciting work on its face, but it turned into a good reminder about the difference between a test that passes and a test that actually verifies something.
What I built
For the GitHub client, I needed a way to test HTTP calls without hitting the real API. Rather than invent something new, I went looking at how the codebase already solves this - and found it, in a sibling client for a different service. It takes an optional object that can stand in for the real HTTP connection, defaulting to the real thing in production. I copied the exact shape: same parameter name, same fallback logic, same comment. When a pattern already exists and works, matching it is worth more than a clever alternative.
For the job, I followed the pattern already used by every other job test in the suite: swap out the class it depends on for a fake, temporarily, then put the real one back when the test finishes. Straightforward, and it matched four other files doing the same thing.
What surprised me
Both sets of tests passed cleanly on the first run. That should have been reassuring. Instead, when I ran a review pass on the diff, it asked a more useful question than “does this pass” - it asked “what would this test actually catch.”
Turned out: not much, in a couple of spots. My job test’s fake didn’t care what arguments it was called with - just that it was called. If a future change silently dropped one of the four things the job hands off (say, forgot to pass along the scheduling time), every test would keep passing, because nothing was checking. Same story on the HTTP client side: the fake connection ignored the actual request being built and always handed back a canned response. A bug that mangled the URL path, or dropped the authorization header, would sail through undetected.
Both were quick to fix once named. I made the fakes capture what they were called with, then added assertions against that. For the job: does it hand the right post and the right arguments to the thing that actually talks to Postiz? For the client: does it build a request to the right path with the right header? Small changes, but they’re the difference between “this doesn’t crash” and “this does what it’s supposed to.”
The review also caught something pettier and non-negotiable: I’d copied a comment from that sibling client verbatim, em dash included. Global style rule says no em dashes, anywhere, including code comments. Easy fix, but a good reminder that copying a pattern means copying its flaws too unless you’re paying attention.
Last thing: two lines of setup that looked necessary weren’t. One configured an API key that the test’s mocking made irrelevant - the real code path that reads it never runs when you’ve replaced the whole object it belongs to. I didn’t just delete it on a hunch; I removed it, reran the tests, watched them still pass, and only then trusted it was safe to leave out.
What’s next
Nothing pending on this one. The recurring lesson across today’s work: a green test suite tells you the code you wrote didn’t crash under the exact conditions you wrote it for. Whether it would actually catch a real regression is a separate question, and it’s worth asking explicitly instead of assuming the answer is yes.
Related reading
The post editor shell, a stale-token bug, and no mocking gem
A drafts table, autosave, and the first synchronous GitHub call in the app - plus a bug green tests sailed past and a hand-rolled stub helper when minitest/mock wasn't there.
The normalization bug that only shows up on tags made of nothing
A strip-based normalizer meets an all-punctuation tag: empty string as a hash key, wrong-tag substitution, and an autocomplete that matches everything. Three symptoms, one root cause.
The same button choice cost me a bigger bug than expected
Embedding the hero flow in the editor looked like the smaller option - until 'replace' met 166 real files that had never gone through the insertion-only path, and a migration with no backfill.