<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Appstruct</title>
    <description>The latest articles on DEV Community by Appstruct (@appstruct_support_4d5c05b).</description>
    <link>https://dev.to/appstruct_support_4d5c05b</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4137932%2F263ef811-fcef-44c3-b987-5124e3f85ddc.png</url>
      <title>DEV Community: Appstruct</title>
      <link>https://dev.to/appstruct_support_4d5c05b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/appstruct_support_4d5c05b"/>
    <language>en</language>
    <item>
      <title>The test was green. The code had never worked.</title>
      <dc:creator>Appstruct</dc:creator>
      <pubDate>Wed, 23 Sep 2026 15:24:00 +0000</pubDate>
      <link>https://dev.to/appstruct_support_4d5c05b/the-test-was-green-the-code-had-never-worked-4jog</link>
      <guid>https://dev.to/appstruct_support_4d5c05b/the-test-was-green-the-code-had-never-worked-4jog</guid>
      <description>&lt;p&gt;Last week I wrote a test, watched it pass, and shipped a feature that was broken the entire time.&lt;/p&gt;

&lt;p&gt;The test wasn't wrong. It asserted exactly the right thing, in the right words, about the right values. It just never touched the code it was supposed to be testing — and nothing about a green checkmark tells you that.&lt;/p&gt;

&lt;p&gt;Here's the shape of it, because once you've seen it you'll find it in your own repo within the hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We build authorize URLs for a pile of OAuth providers. Most of them want space-separated scopes, because that's what OAuth 2.0 specifies. A couple of them — Meta's Threads, TikTok — document a comma instead.&lt;/p&gt;

&lt;p&gt;Getting this wrong is not loud. The whole scope string arrives as one unrecognised scope, so the consent screen grants nothing, and you find out much later from a permissions error that points somewhere else entirely.&lt;/p&gt;

&lt;p&gt;So the separator became a property of the provider:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;// oauth-providers.ts&lt;br&gt;
export const PROVIDERS = {&lt;br&gt;
  alpha: { scopes: ['read', 'write'] },                       // space, the default&lt;br&gt;
  beta:  { scopes: ['read', 'write'], scopeSeparator: ',' },  // comma&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
&lt;code&gt;// controller.ts&lt;br&gt;
const params = new URLSearchParams({&lt;br&gt;
  scope: provider.scopes.join(provider.scopeSeparator ?? ' '),&lt;br&gt;
  // ...&lt;br&gt;
})&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And a test:&lt;br&gt;
`const joined = (key: string) =&amp;gt; {&lt;br&gt;
  const p = PROVIDERS[key]&lt;br&gt;
  return p.scopes.join(p.scopeSeparator ?? ' ')&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;it('beta joins with a comma, as the docs require', () =&amp;gt; {&lt;br&gt;
  expect(joined('beta')).toBe('read,write')&lt;br&gt;
})&lt;/p&gt;

&lt;p&gt;it('alpha keeps the OAuth 2.0 default', () =&amp;gt; {&lt;br&gt;
  expect(joined('alpha')).toBe('read write')&lt;br&gt;
})`&lt;/p&gt;

&lt;p&gt;Green. Both of them. Ship it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Read the test helper again:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;return p.scopes.join(p.scopeSeparator ?? ' ')&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That's not a call into the controller. That's a second copy of the controller's logic, living in the test file.&lt;/p&gt;

&lt;p&gt;Which means: go back to controller.ts, change it to the old hardcoded .join(' '), and run the suite.&lt;/p&gt;

&lt;p&gt;Still green.&lt;/p&gt;

&lt;p&gt;The tests assert that PROVIDERS is shaped correctly, and it is. They say nothing whatsoever about the code that builds the URL. I could delete the separator from the controller entirely and ship a broken integration with a full green run behind it.&lt;/p&gt;

&lt;p&gt;I know this because I did exactly that, on purpose, to check — and I very nearly didn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is boring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One implementation, called by both:&lt;/p&gt;

&lt;p&gt;`// oauth-providers.ts&lt;br&gt;
export const scopeParam = (p: OAuthProvider): string =&amp;gt;&lt;br&gt;
  p.scopes.join(p.scopeSeparator ?? ' ')&lt;br&gt;
// controller.ts&lt;br&gt;
scope: scopeParam(provider),&lt;/p&gt;

&lt;p&gt;// the test&lt;br&gt;
const joined = (key: string) =&amp;gt; scopeParam(PROVIDERS[key])`&lt;/p&gt;

&lt;p&gt;Nothing about the assertions changed. The expect lines are identical. But now breaking that join turns three tests red instead of zero, because there is only one join left to break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule I use now&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For every test you write, name the single line of source whose mutation turns it red.&lt;/p&gt;

&lt;p&gt;If you can't name that line, you haven't written a test. You've written a description of the current behaviour that will agree with any future behaviour too.&lt;/p&gt;

&lt;p&gt;And then — this is the part people skip — actually go and break that line. Watch it fail. Put it back. It takes twenty seconds and it is the only thing that separates a regression guard from decoration.&lt;/p&gt;

&lt;p&gt;I now do this for anything load-bearing. It's caught a lot:&lt;/p&gt;

&lt;p&gt;A picker that looked up a pending record by id, with the owner check as a separate if afterwards. Deleting the check kept the suite green. Moving the owner into the where clause made two tests fail when removed.&lt;br&gt;
An adapter that called a token-upgrade endpoint and then handed the old token to everything downstream. Perfectly plausible in review. One mutation, one red test.&lt;br&gt;
Neither of those is a clever bug. Both are the kind that ship.&lt;/p&gt;

&lt;p&gt;The second trap: asserting the wrong true thing&lt;br&gt;
There's a cousin of this worth naming, because mutation testing won't save you from it.&lt;/p&gt;

&lt;p&gt;We store an OAuth token with an expiry. One provider's token exchange returns a token that lives one hour — and the response carries no expires_in field at all, so nothing in it announces that. Our code stored the token and wrote a 60-day expiry next to it, a number we had made up.&lt;/p&gt;

&lt;p&gt;A test like this passes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;expect(info.expiresAt).toBeInstanceOf(Date)&lt;br&gt;
expect(daysUntil(info.expiresAt)).toBeCloseTo(60)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It is green, it mutates correctly, and it is worthless — because it asserts that the code does what the code does. The constant under test and the constant in the assertion came from the same place: a guess.&lt;/p&gt;

&lt;p&gt;What it produced in practice: connect the account, publish fine for an hour, then fail every scheduled job for two months while the UI shows a healthy connection.&lt;/p&gt;

&lt;p&gt;The only test that helps here has an oracle from outside the code — in this case the provider's documented lifetime, and the exchange call you're supposed to make. The test stopped being "is expiresAt set" and became "did we perform the upgrade, and is the stored token the upgraded one, not the short-lived one".&lt;/p&gt;

&lt;p&gt;Mutation testing checks that your test is connected to your code. It cannot check that your code is connected to reality. Those are two different jobs and you need both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it on your own repo&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pick the test you'd be most upset to lose. Open the file it covers. Break one line — flip a boolean, drop a !, hardcode a return.&lt;/p&gt;

&lt;p&gt;Run the suite.&lt;/p&gt;

&lt;p&gt;If it's still green, you just learned something more useful than the test ever told you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building AppStruct — a workspace where the docs, spreadsheets, slides, whiteboards, CRM and team chat are one product instead of six subscriptions that don't talk to each other. The bugs above are from its integration layer, which is where the interesting ones live. If you want to see what the engineering behind it actually looks like, that's the honest sample.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codequality</category>
      <category>debugging</category>
      <category>softwaredevelopment</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
