<?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: Tzuri</title>
    <description>The latest articles on DEV Community by Tzuri (@tzurid25).</description>
    <link>https://dev.to/tzurid25</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%2F4043992%2Fc71ed795-7e01-448a-8302-cfaa59e56c41.png</url>
      <title>DEV Community: Tzuri</title>
      <link>https://dev.to/tzurid25</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tzurid25"/>
    <language>en</language>
    <item>
      <title>Your AI-written tests pass. That doesn't mean they work.</title>
      <dc:creator>Tzuri</dc:creator>
      <pubDate>Thu, 23 Jul 2026 14:31:09 +0000</pubDate>
      <link>https://dev.to/tzurid25/your-ai-written-tests-pass-that-doesnt-mean-they-work-h60</link>
      <guid>https://dev.to/tzurid25/your-ai-written-tests-pass-that-doesnt-mean-they-work-h60</guid>
      <description>&lt;p&gt;Here's a function. Three branches, two thresholds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;healthy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's a test suite for it, of the kind an AI assistant will happily produce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;returns a value for a high score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeDefined&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;returns a value for a mid score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeDefined&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;returns a value for a low score&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeDefined&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three tests. Every line executed. &lt;strong&gt;Coverage: 100%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now change &lt;code&gt;&amp;gt;=&lt;/code&gt; to &lt;code&gt;&amp;gt;&lt;/code&gt; on line 2. Change it to &lt;code&gt;&amp;lt;&lt;/code&gt;. Replace the whole condition with &lt;code&gt;true&lt;/code&gt;. Delete the second branch entirely.&lt;/p&gt;

&lt;p&gt;The tests still pass. All of them. Every time.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;toBeDefined()&lt;/code&gt; doesn't check what the function returned — only that it returned &lt;em&gt;something&lt;/em&gt;. The tests execute the code. They never verify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage measures the wrong thing
&lt;/h2&gt;

&lt;p&gt;Coverage answers: &lt;em&gt;did a test run this line?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The question you actually care about: &lt;em&gt;would a test fail if this line were wrong?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Those are not the same question, and the gap between them is where bugs live. A line can be covered by twenty tests and still be completely unprotected.&lt;/p&gt;

&lt;p&gt;This has always been true. What changed is the volume. When a human writes a weak assertion, it's usually one, and usually because they were tired. When an assistant generates a suite, the weak assertions arrive by the file — and they arrive looking &lt;em&gt;correct&lt;/em&gt;. &lt;code&gt;toBeDefined()&lt;/code&gt;, &lt;code&gt;expect(fn).toHaveBeenCalled()&lt;/code&gt;, &lt;code&gt;expect(result).toBeTruthy()&lt;/code&gt;. These read like tests. They pass code review. They light up the coverage report green.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mutation testing asks the right question
&lt;/h2&gt;

&lt;p&gt;Mutation testing changes your source code on purpose — flips a &lt;code&gt;&amp;gt;=&lt;/code&gt; to &lt;code&gt;&amp;gt;&lt;/code&gt;, turns a condition into &lt;code&gt;true&lt;/code&gt;, negates a boolean — and re-runs your suite against each change.&lt;/p&gt;

&lt;p&gt;If a test fails, good: something was actually checking that behaviour. The mutant is &lt;strong&gt;killed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If every test still passes, that behaviour is executed but unverified. The mutant &lt;strong&gt;survived&lt;/strong&gt;, and you've found a real gap.&lt;/p&gt;

&lt;p&gt;It's a decades-old technique. The reason it isn't standard practice is cost: mutating a whole repository means running your test suite hundreds or thousands of times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope it to the diff
&lt;/h2&gt;

&lt;p&gt;You don't need to mutate the whole repository. You need to know whether the code &lt;em&gt;in this pull request&lt;/em&gt; is actually tested.&lt;/p&gt;

&lt;p&gt;That's the idea behind &lt;a href="https://github.com/tzurid25/aitg" rel="noopener noreferrer"&gt;aitg&lt;/a&gt;: take the changed lines from &lt;code&gt;git diff&lt;/code&gt;, mutate only those, and report on them. Minutes become seconds. It runs on &lt;a href="https://stryker-mutator.io/" rel="noopener noreferrer"&gt;StrykerJS&lt;/a&gt;, which does the actual mutation work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @aitg/cli @stryker-mutator/core
npx aitg init &lt;span class="nt"&gt;--local&lt;/span&gt;   &lt;span class="c"&gt;# no account, nothing uploaded&lt;/span&gt;
npx aitg scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the classifier above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mutation testing complete (vitest, 67 mutants).

Results
  Mutation score: 0%
  Killed:       0
  Survived:     8

Surviving mutants
  ! classify.js:2 (ConditionalExpression)
  ! classify.js:2 (EqualityOperator)
  ! classify.js:3 (ConditionalExpression)
  ! classify.js:3 (EqualityOperator)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;100% coverage. 0% mutation score. Eight ways to break the code that no test would catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output has to be actionable
&lt;/h2&gt;

&lt;p&gt;A list of surviving mutants isn't useful on its own. So the tool writes a prompt you can paste into whatever assistant you use, with each gap explained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- if (score &amp;gt;= 80) return "healthy";
&lt;/span&gt;&lt;span class="gi"&gt;+ score &amp;gt; 80
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;A comparison boundary was shifted (inclusive/exclusive) and no test noticed, so the exact edge value is untested. Assert the boundary itself, not just values on either side.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;versus, on the same line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- if (score &amp;gt;= 80) return "healthy";
&lt;/span&gt;&lt;span class="gi"&gt;+ score &amp;lt; 80
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;A comparison was reversed and no test noticed, so the direction of the check is unverified.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those need different tests. &lt;code&gt;&amp;gt;=&lt;/code&gt; to &lt;code&gt;&amp;gt;&lt;/code&gt; is only distinguishable at exactly &lt;code&gt;80&lt;/code&gt;. &lt;code&gt;&amp;gt;=&lt;/code&gt; to &lt;code&gt;&amp;lt;&lt;/code&gt; is distinguishable almost anywhere. Telling a developer "an equality check was inverted" for the first case sends them to write the wrong test.&lt;/p&gt;

&lt;p&gt;The explanation is derived from the actual mutation, not its category. This matters more than it sounds: an explanation that visibly contradicts the diff printed above it costs you trust in every other line of the report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uncovered code is a different problem
&lt;/h2&gt;

&lt;p&gt;If nothing reaches a line, no mutant on that line can survive — it never runs. So a file with &lt;em&gt;zero&lt;/em&gt; tests can look cleaner than a file with weak ones.&lt;/p&gt;

&lt;p&gt;That's backwards, so uncovered code gets its own section, and the distinction is spelled out: a survivor means tighten an assertion that already exists; no-coverage means write a test that reaches the code at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't do
&lt;/h2&gt;

&lt;p&gt;It won't tell you your architecture is wrong, or that you're testing the wrong things entirely. It measures one specific property: whether your assertions would notice if the code changed.&lt;/p&gt;

&lt;p&gt;It also needs a passing test suite to start from. Mutation testing compares mutated runs against a green baseline. No baseline, no signal.&lt;/p&gt;

&lt;p&gt;And it's slower than coverage. Scoped to a diff it's seconds, but it's not free.&lt;/p&gt;

&lt;h2&gt;
  
  
  On testing the tool itself
&lt;/h2&gt;

&lt;p&gt;A tool that measures test quality had better have decent tests. It has 54.&lt;/p&gt;

&lt;p&gt;More to the point, those tests were validated by mutation rather than by counting coverage: six defects fixed during development were deliberately reintroduced, and each had to make tests fail.&lt;/p&gt;

&lt;p&gt;One didn't. It checked that the diff engine compares against the merge-base rather than the branch tip — and it passed under both. The divergent commit in the fixture &lt;em&gt;added&lt;/em&gt; a file, which shows up as a deletion when diffing against the tip, and deletions get skipped anyway. Both strategies produced identical visible results.&lt;/p&gt;

&lt;p&gt;The test looked correct. It passed. It verified nothing.&lt;/p&gt;

&lt;p&gt;It now modifies a shared file instead, and catches the mutation. But that's exactly the failure mode described at the top of this post, found in the suite of the tool built to detect it. If it can hide there, it can hide anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx aitg init &lt;span class="nt"&gt;--local&lt;/span&gt;
npx aitg scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it on your most recent PR. If the mutation score comes back near your coverage number, your tests are doing their job. If there's a gap, that gap is where the bugs get through.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/@aitg/cli" rel="noopener noreferrer"&gt;&lt;code&gt;@aitg/cli&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/tzurid25/aitg" rel="noopener noreferrer"&gt;tzurid25/aitg&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIT licensed. The CLI works entirely offline — no account, nothing uploaded.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>javascript</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
