<?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: r4mimu</title>
    <description>The latest articles on DEV Community by r4mimu (@r4mimu).</description>
    <link>https://dev.to/r4mimu</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3858473%2F70db7fef-9864-42a3-8ccd-97b2d26d759a.jpg</url>
      <title>DEV Community: r4mimu</title>
      <link>https://dev.to/r4mimu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/r4mimu"/>
    <language>en</language>
    <item>
      <title>gomod-age: A Simple CI Gate Against Go Dependency Supply Chain Attacks</title>
      <dc:creator>r4mimu</dc:creator>
      <pubDate>Sat, 04 Apr 2026 12:08:14 +0000</pubDate>
      <link>https://dev.to/r4mimu/gomod-age-a-simple-ci-gate-against-go-dependency-supply-chain-attacks-4dbb</link>
      <guid>https://dev.to/r4mimu/gomod-age-a-simple-ci-gate-against-go-dependency-supply-chain-attacks-4dbb</guid>
      <description>&lt;h2&gt;
  
  
  The Problem Nobody Talks About Until It's Too Late
&lt;/h2&gt;

&lt;p&gt;Here's a scenario that keeps Go developers up at night: someone publishes a malicious package to a module proxy, and your CI pipeline happily pulls it in on the next &lt;code&gt;go mod tidy&lt;/code&gt;. The package is minutes old, has zero adoption, and contains a backdoor. Your tests pass. Your linter is green. You ship it to production.&lt;/p&gt;

&lt;p&gt;This isn't hypothetical. Supply chain attacks targeting package registries have been climbing year over year. The &lt;code&gt;event-stream&lt;/code&gt; incident in npm, the &lt;code&gt;ua-parser-js&lt;/code&gt; hijack, the &lt;code&gt;colors.js&lt;/code&gt; sabotage — these are well-known cases, but the Go ecosystem isn't immune. Typosquatting, account takeovers, and dependency confusion attacks all apply.&lt;/p&gt;

&lt;p&gt;Most teams rely on &lt;code&gt;go.sum&lt;/code&gt; for integrity checks and &lt;code&gt;GOPRIVATE&lt;/code&gt; for internal modules. That covers "did the bits change?" but not "should we trust this release at all?" There's a gap between a version being published and a version being safe to consume. Nothing in the standard Go toolchain guards that window.&lt;/p&gt;

&lt;h2&gt;
  
  
  What gomod-age Does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/fchimpan/gomod-age" rel="noopener noreferrer"&gt;gomod-age&lt;/a&gt; is a single-binary CLI that checks whether your Go dependencies have been published long enough. If a module version is younger than a configurable threshold (default: 3 days), the check fails. That's it. No accounts to create, no SaaS to integrate, no vulnerability database to query.&lt;/p&gt;

&lt;p&gt;The idea is dead simple: legitimate releases age like wine. Malicious releases get reported and yanked quickly. By enforcing a quarantine period, you let the community's immune system do its job before you consume a new version.&lt;/p&gt;

&lt;p&gt;It queries the Go module proxy (&lt;code&gt;GOPROXY&lt;/code&gt;) for the publish timestamp of each dependency, compares it against the threshold, and exits non-zero if anything is too fresh. The entire check runs in seconds thanks to concurrent proxy queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Fits in Your Workflow
&lt;/h2&gt;

&lt;p&gt;gomod-age is designed as a CI gate, not a development-time annoyance. You run it in pull request checks alongside your tests and lints. When a PR adds or updates a dependency, gomod-age tells you if any of those versions were published too recently.&lt;/p&gt;

&lt;p&gt;The typical CI pattern looks like this with GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v5&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;go-version-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go.mod&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go install github.com/fchimpan/gomod-age@latest&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gomod-age -base origin/${{ github.base_ref }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-base&lt;/code&gt; flag is the key here. Instead of checking every dependency in your &lt;code&gt;go.mod&lt;/code&gt;, it diffs against the base branch and only checks modules that changed. This means existing dependencies don't trigger false positives just because you happen to run the check within 3 days of their release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/fchimpan/gomod-age@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it locally to see what it reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check all dependencies&lt;/span&gt;
gomod-age

&lt;span class="c"&gt;# Only check what changed relative to main&lt;/span&gt;
gomod-age &lt;span class="nt"&gt;-base&lt;/span&gt; origin/main

&lt;span class="c"&gt;# Bump the threshold to 7 days&lt;/span&gt;
gomod-age &lt;span class="nt"&gt;-age&lt;/span&gt; 7d

&lt;span class="c"&gt;# Include indirect dependencies too&lt;/span&gt;
gomod-age &lt;span class="nt"&gt;-indirect&lt;/span&gt;

&lt;span class="c"&gt;# Machine-readable output&lt;/span&gt;
gomod-age &lt;span class="nt"&gt;-json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The text output is a table showing violations, with the module path, version, publish time, current age, and how much time remains before it clears the threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VIOLATIONS (1):
MODULE                                             VERSION      PUBLISHED              AGE        REMAINING
--------------------------------------------------------------------------------------------------------------
github.com/example/suspiciously-new                v0.1.0       2026-04-03T10:00:00Z   1d2h       1d22h

Summary: 42 passed, 1 violations, 3 skipped, 0 errors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit codes are straightforward: 0 means all clear, 1 means violations found, 2 means a tool error (bad flags, network issues, etc.).&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Real-World Complexity
&lt;/h2&gt;

&lt;p&gt;A few things that come up in practice:&lt;/p&gt;

&lt;h3&gt;
  
  
  Private modules
&lt;/h3&gt;

&lt;p&gt;gomod-age reads &lt;code&gt;GOPRIVATE&lt;/code&gt; and automatically skips those modules. Internal packages hosted on your own registry don't need age checks — you control when they're published.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ignoring specific modules
&lt;/h3&gt;

&lt;p&gt;Sometimes you know a freshly-published module is fine. Maybe your team owns it, or you've reviewed the release manually. Use the &lt;code&gt;-ignore&lt;/code&gt; flag with glob patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gomod-age &lt;span class="nt"&gt;-ignore&lt;/span&gt; &lt;span class="s2"&gt;"github.com/my-org/*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Allow list for reviewed versions
&lt;/h3&gt;

&lt;p&gt;For one-off exceptions, the config file supports an &lt;code&gt;allow&lt;/code&gt; list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .gomod-age.yaml&lt;/span&gt;
&lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;7d&lt;/span&gt;
&lt;span class="na"&gt;ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;github.com/my-org/*"&lt;/span&gt;
&lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.com/new-lib/foo&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1.2.0&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PR&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;#42"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The allow entry is pinned to an exact &lt;a href="mailto:module@version"&gt;module@version&lt;/a&gt;. When the version changes, the age check kicks in again. This prevents a "reviewed once, trusted forever" blind spot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replace directives
&lt;/h3&gt;

&lt;p&gt;If your &lt;code&gt;go.mod&lt;/code&gt; uses &lt;code&gt;replace&lt;/code&gt; directives, gomod-age checks the replacement module's publish time, not the original. This handles the common pattern of forking a dependency while maintaining accurate age checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  GOPROXY chain
&lt;/h3&gt;

&lt;p&gt;gomod-age respects the full &lt;code&gt;GOPROXY&lt;/code&gt; fallback chain, including the difference between comma separators (fall through on 404/410 only) and pipe separators (fall through on any error). If you're running a private proxy like Athens in front of &lt;code&gt;proxy.golang.org&lt;/code&gt;, it just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Doesn't Do
&lt;/h2&gt;

&lt;p&gt;gomod-age is not a vulnerability scanner. It doesn't check CVE databases, it doesn't analyze source code, and it doesn't score packages by reputation. Tools like &lt;code&gt;govulncheck&lt;/code&gt; and Snyk handle that.&lt;/p&gt;

&lt;p&gt;It's a single, narrow check: "is this version old enough to trust?" That narrowness is a feature. It composes with your existing security tools rather than trying to replace them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Supply chain security is a layered problem. &lt;code&gt;go.sum&lt;/code&gt; handles integrity. &lt;code&gt;govulncheck&lt;/code&gt; handles known vulnerabilities. Code review handles logic. But none of these catch a malicious version in the hours after it's published and before it's flagged.&lt;/p&gt;

&lt;p&gt;gomod-age fills that gap with a 30-second check that requires zero configuration. Add it to your CI pipeline, forget about it, and let it catch the dependency updates that deserve a closer look.&lt;/p&gt;

&lt;p&gt;The source is at &lt;a href="https://github.com/fchimpan/gomod-age" rel="noopener noreferrer"&gt;github.com/fchimpan/gomod-age&lt;/a&gt;. It's a single &lt;code&gt;go install&lt;/code&gt; away.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>go</category>
      <category>security</category>
    </item>
    <item>
      <title>Your Go Tests Pass, But Do They Actually Test Anything? An Introduction to Mutation Testing</title>
      <dc:creator>r4mimu</dc:creator>
      <pubDate>Fri, 03 Apr 2026 00:09:32 +0000</pubDate>
      <link>https://dev.to/r4mimu/your-go-tests-pass-but-do-they-actually-test-anything-an-introduction-to-mutation-testing-1k9l</link>
      <guid>https://dev.to/r4mimu/your-go-tests-pass-but-do-they-actually-test-anything-an-introduction-to-mutation-testing-1k9l</guid>
      <description>&lt;p&gt;GitHub Copilot, Cursor, Claude Code — these tools can generate hundreds of lines of Go in seconds. But there's a problem that doesn't get enough attention: AI-generated code ships with AI-generated confidence, not correctness.&lt;/p&gt;

&lt;p&gt;Your test suite says "all green." Your coverage report says 85%. But how many of those tests actually catch real bugs? How many are just going through the motions — executing code paths without verifying behavior?&lt;/p&gt;

&lt;p&gt;This is where mutation testing comes in. And this is why I built &lt;a href="https://github.com/fchimpan/mutest" rel="noopener noreferrer"&gt;mutest&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hidden Cost of AI-Assisted Development
&lt;/h2&gt;

&lt;p&gt;AI coding assistants are good at producing code that &lt;em&gt;looks&lt;/em&gt; correct. They generate functions with proper signatures, idiomatic error handling, and even test files. But there's a gap: AI tends to produce tests that cover code paths rather than verify boundaries.&lt;/p&gt;

&lt;p&gt;Consider this function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;IsEligibleForDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An AI might generate tests like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestIsEligibleForDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&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="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;IsEligibleForDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"IsEligibleForDiscount(15) = %v, want true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&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="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;IsEligibleForDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"IsEligibleForDiscount(3) = %v, want false"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&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;Coverage: 100%. Every line is executed. CI is green. Ship it.&lt;/p&gt;

&lt;p&gt;But what happens if someone accidentally changes &lt;code&gt;quantity &amp;gt;= 10&lt;/code&gt; to &lt;code&gt;quantity &amp;gt; 10&lt;/code&gt;? Both tests still pass. The boundary case — &lt;code&gt;IsEligibleForDiscount(10)&lt;/code&gt; — was never tested. A customer ordering exactly 10 items would silently lose their discount, and your test suite would never notice.&lt;/p&gt;

&lt;p&gt;This is not a contrived example. Boundary-value and equality bugs are among the most common defects in production software, and they're the kind of bug that AI-generated tests routinely miss.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mutation Testing: The Test for Your Tests
&lt;/h2&gt;

&lt;p&gt;Mutation testing flips the question. Instead of "does my code pass the tests?", it asks "do my tests catch bugs?"&lt;/p&gt;

&lt;p&gt;The concept is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take your source code&lt;/li&gt;
&lt;li&gt;Introduce a small, deliberate change (a "mutation") — like swapping &lt;code&gt;&amp;gt;=&lt;/code&gt; for &lt;code&gt;&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run your test suite against the mutated code&lt;/li&gt;
&lt;li&gt;If the tests fail, the mutant is "killed" — your tests caught the bug&lt;/li&gt;
&lt;li&gt;If the tests pass, the mutant has "survived" — you have a test gap&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every surviving mutant points to a specific line where a bug could hide undetected. Unlike line coverage, which just tells you what code ran, this tells you whether your tests actually noticed the change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters Now
&lt;/h3&gt;

&lt;p&gt;In the pre-AI era, developers wrote code slowly and typically had intuition about boundary conditions because they had reasoned through the logic themselves. AI-generated code skips that reasoning step. The code appears, the tests appear, and the developer reviews both — but the boundary considerations are often missing from both.&lt;/p&gt;

&lt;p&gt;Mutation testing fills that gap. It's the automated version of a senior engineer asking, "But what happens when the value is exactly 10?"&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with Existing Mutation Testing Tools
&lt;/h2&gt;

&lt;p&gt;If mutation testing is so valuable, why isn't everyone doing it? Because existing tools make it impractical for real-world CI pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  They're Too Slow
&lt;/h3&gt;

&lt;p&gt;Traditional mutation testing tools generate thousands of mutants — mutating arithmetic operators, boolean returns, assignments, method calls, and more. For a medium-sized Go project, this easily means 5,000-10,000+ mutants. Each one requires recompilation and a full test run. Even with optimization, you're looking at 30-60 minutes of CI time.&lt;/p&gt;

&lt;p&gt;Nobody runs a 45-minute mutation test on every pull request.&lt;/p&gt;

&lt;h3&gt;
  
  
  They're Too Noisy
&lt;/h3&gt;

&lt;p&gt;More mutation operators mean more surviving mutants — but not all surviving mutants represent real test gaps. Mutating &lt;code&gt;x + y&lt;/code&gt; to &lt;code&gt;x - y&lt;/code&gt; in a logging format string is technically a surviving mutant, but it doesn't point to a meaningful test gap. When your mutation report has 200 survivors and only 15 are actionable, developers stop reading the report.&lt;/p&gt;

&lt;h3&gt;
  
  
  Go-Specific Pain Points
&lt;/h3&gt;

&lt;p&gt;The Go ecosystem has a few mutation testing tools, but they share common limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Per-mutant recompilation: Most tools modify source files and rebuild for every single mutant. Go's compilation is fast, but &lt;code&gt;N mutations * (compile + test)&lt;/code&gt; adds up quickly across a whole project.&lt;/li&gt;
&lt;li&gt;Source file modification: Some tools directly modify &lt;code&gt;.go&lt;/code&gt; files, creating race conditions with IDE file watchers, breaking &lt;code&gt;gopls&lt;/code&gt;, and risking corrupted source if the process is interrupted mid-run.&lt;/li&gt;
&lt;li&gt;Kitchen-sink mutation strategies: Trying to mutate everything — arithmetic, assignments, returns, conditionals — generates way too many mutants, most of which aren't useful.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: developers try mutation testing once, wait 20 minutes for a noisy report, and never run it again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing mutest
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/fchimpan/mutest" rel="noopener noreferrer"&gt;mutest&lt;/a&gt; is a mutation testing tool for Go that takes a different approach. Instead of mutating everything, it focuses on the operators that actually matter — and runs fast enough for CI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mutest ./...
&lt;span class="go"&gt;mutest: discovered 4 mutation points
mutest: testing with 10 workers, 30s timeout per mutant

&lt;/span&gt;&lt;span class="gp"&gt;--- SURVIVED: calc.go:5:7  &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;to &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;0.21s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;--- SURVIVED: calc.go:21:7  &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;to &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;0.21s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;--- SURVIVED: calc.go:18:7  &amp;lt; to &amp;lt;= (0.21s)
&lt;/span&gt;&lt;span class="gp"&gt;--- KILLED: calc.go:13:11  &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;to &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;0.63s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;
===== Mutation Testing Summary =====
Total:     4
Killed:    1
Survived:  3
Score:     25.0%
Duration:  633ms
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;633 milliseconds. Not minutes. Milliseconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes mutest Different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Focused Mutation Strategy
&lt;/h3&gt;

&lt;p&gt;mutest targets Relational Operator Replacement (ROR) — comparison and equality operators only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;  →  &amp;gt;=
&amp;gt;=  →  &amp;gt;
&amp;lt;  →  &amp;lt;=
&amp;lt;=  →  &amp;lt;
==  →  !=
!=  →  ==
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't a limitation — it's a deliberate choice. Off-by-one and equality bugs are some of the most common defects in production Go code, and these six mutations target exactly those. The academic literature on mutation testing backs this up: ROR is one of the most cost-effective mutation operator subsets.&lt;/p&gt;

&lt;p&gt;By limiting scope, mutest keeps the mutant count small — typically 10-50 per package instead of thousands. Every survivor is something you should actually look at.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Runtime Mutation Selection (Compile Once, Run Many)
&lt;/h3&gt;

&lt;p&gt;Traditional tools follow this pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For each mutant:
    1. Modify source file
    2. Compile package
    3. Run tests
    4. Restore source file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;mutest does it differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For each package:
    1. Instrument all mutation points with generic helper functions
    2. Compile ONE test binary with all mutations embedded
For each mutant:
    3. Run the pre-built binary with MUTEST_ID=N
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, a comparison like &lt;code&gt;a &amp;gt; b&lt;/code&gt; is replaced with a generic helper function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;_mutest_cmp_1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;cmp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordered&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_mutest_init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_mutest_active&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;  &lt;span class="c"&gt;// mutated&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;  &lt;span class="c"&gt;// original&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package is compiled once with all these helpers embedded. Each mutant is then activated by running the same binary with a different environment variable: &lt;code&gt;MUTEST_ID=1&lt;/code&gt;, &lt;code&gt;MUTEST_ID=2&lt;/code&gt;, and so on.&lt;/p&gt;

&lt;p&gt;The cost goes from &lt;code&gt;N mutations * (compile + run)&lt;/code&gt; to &lt;code&gt;P packages * compile + N mutations * run&lt;/code&gt;. For a package with 30 mutations, that's 1 compilation instead of 30.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Non-Destructive by Design
&lt;/h3&gt;

&lt;p&gt;mutest never modifies your source files. All instrumented code lives in temporary files, and Go's &lt;code&gt;-overlay&lt;/code&gt; flag tells the compiler to use them instead of the originals. If the process is killed, interrupted, or crashes — your source code is untouched. Your IDE stays happy. Your &lt;code&gt;git status&lt;/code&gt; stays clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Smart Noise Reduction
&lt;/h3&gt;

&lt;p&gt;mutest automatically skips mutations that would produce false positives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;len(x) &amp;gt; 0&lt;/code&gt; to &lt;code&gt;len(x) &amp;gt;= 0&lt;/code&gt;: &lt;code&gt;len()&lt;/code&gt; never returns a negative value, so this mutation can never change behavior. Skipped.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cap(x) &amp;gt; 0&lt;/code&gt; to &lt;code&gt;cap(x) &amp;gt;= 0&lt;/code&gt;: Same reasoning — &lt;code&gt;cap()&lt;/code&gt; is always non-negative. Skipped.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;if err != nil { return err }&lt;/code&gt;: Go's idiomatic error propagation. Mutating these generates noise without revealing meaningful test gaps. Skipped by default (configurable with &lt;code&gt;-skip-err-propagation=false&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Comparisons against non-zero values (like &lt;code&gt;len(s) &amp;gt; 1&lt;/code&gt;) are not skipped — the boundary between 1 and 2 matters and should be tested.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Zero External Dependencies
&lt;/h3&gt;

&lt;p&gt;mutest is built entirely on the Go standard library — &lt;code&gt;go/ast&lt;/code&gt;, &lt;code&gt;go/parser&lt;/code&gt;, &lt;code&gt;go/token&lt;/code&gt;, &lt;code&gt;os/exec&lt;/code&gt;, and friends. No third-party dependencies. &lt;code&gt;go install&lt;/code&gt; and you're done.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started with mutest
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/fchimpan/mutest@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires Go 1.24 or later. Pre-built binaries for Linux, macOS, and Windows are also available on the &lt;a href="https://github.com/fchimpan/mutest/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt; page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run against all packages (same syntax as go test)&lt;/span&gt;
mutest ./...

&lt;span class="c"&gt;# Target a specific package&lt;/span&gt;
mutest ./pkg/handler

&lt;span class="c"&gt;# Show test output for each mutant&lt;/span&gt;
mutest &lt;span class="nt"&gt;-v&lt;/span&gt; ./...

&lt;span class="c"&gt;# Preview mutations without running tests&lt;/span&gt;
mutest &lt;span class="nt"&gt;-dry-run&lt;/span&gt; ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output format mirrors &lt;code&gt;go test&lt;/code&gt;, so it feels familiar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;--- KILLED: handler.go:25:11  &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;to &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;0.42s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;--- SURVIVED: handler.go:44:9  &amp;lt; to &amp;lt;= (0.19s)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CI Integration
&lt;/h3&gt;

&lt;p&gt;mutest is designed to work as a CI quality gate. Two practical patterns:&lt;/p&gt;

&lt;p&gt;Pattern 1 — diff-only mutation testing. Only mutate lines changed in the current pull request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mutest &lt;span class="nt"&gt;-diff&lt;/span&gt; origin/main &lt;span class="nt"&gt;-threshold&lt;/span&gt; 100 ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This says: "For every comparison operator I changed or added, there must be a test that covers the boundary." If any changed comparison survives mutation, the pipeline fails. If the diff contains no mutation targets (e.g., only comments or non-Go files changed), mutest exits 0 — no false failures.&lt;/p&gt;

&lt;p&gt;Pattern 2 — full-project threshold. Set a minimum mutation score for the entire project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mutest &lt;span class="nt"&gt;-threshold&lt;/span&gt; 80 ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GitHub Actions Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mutation Testing&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;  &lt;span class="c1"&gt;# Full history needed for -diff&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;go-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stable&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go install github.com/fchimpan/mutest@latest&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mutest -diff origin/main -threshold 100 ./...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Under a minute of CI time for most projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Controlling Scope with &lt;code&gt;//mutest:skip&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Not every comparison needs mutation testing. Use the &lt;code&gt;//mutest:skip&lt;/code&gt; directive to exclude code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Skip an entire function&lt;/span&gt;
&lt;span class="c"&gt;//mutest:skip&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;legacyCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Skip a block (if/for/switch/select) — skips all nested statements too&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;//mutest:skip&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Canceled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrCanceled&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fetch: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Skip a single line&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;//mutest:skip&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  JSON Output for Tooling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Single JSON summary&lt;/span&gt;
mutest &lt;span class="nt"&gt;-json&lt;/span&gt; ./...

&lt;span class="c"&gt;# Streaming NDJSON (one line per mutant as results arrive)&lt;/span&gt;
mutest &lt;span class="nt"&gt;-json&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON output makes it easy to integrate mutest with custom dashboards, Slack notifications, or code review tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fixing a Survived Mutant
&lt;/h2&gt;

&lt;p&gt;When mutest reports a survivor, it tells you exactly where to look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;--- SURVIVED: pricing.go:12:7  &amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; to &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;0.21s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means mutest changed &lt;code&gt;&amp;gt;=&lt;/code&gt; to &lt;code&gt;&amp;gt;&lt;/code&gt; and no test noticed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;IsEligibleForDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  &lt;span class="c"&gt;// mutest changed this to &amp;gt;, tests still passed&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix — add a test at the boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestIsEligibleForDiscount_ExactlyTen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// This test kills the &amp;gt;= to &amp;gt; mutation because&lt;/span&gt;
    &lt;span class="c"&gt;// IsEligibleForDiscount(10) returns true with &amp;gt;=, but false with &amp;gt;.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;IsEligibleForDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"IsEligibleForDiscount(10) = %v, want true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&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;Re-run mutest and that mutation point will now show &lt;code&gt;--- KILLED&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Impact
&lt;/h2&gt;

&lt;p&gt;I've been running mutest in CI on Go projects. The most obvious thing it finds: off-by-one errors in pagination, boundary checks in rate limiting, equality comparisons that should be inequalities. Bugs that 100% line coverage misses entirely.&lt;/p&gt;

&lt;p&gt;The less obvious effect: it changes how developers write tests. Once you start seeing surviving mutants, you naturally start adding boundary-value tests. You stop writing tests that just exercise the happy path.&lt;/p&gt;

&lt;p&gt;Speed matters more than you'd think here. A mutation testing tool that takes 30 minutes gets disabled after the first sprint. mutest finishes in seconds, so it stays on. And with diff mode, you don't need to retroactively hit a high mutation score across your whole project — just enforce it on new and changed code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;AI coding assistants are making us more productive, but they're also making it easier to ship code with untested boundary conditions. Line coverage tells you what code &lt;em&gt;ran&lt;/em&gt;; mutation testing tells you what code was actually &lt;em&gt;verified&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Think of it this way: &lt;code&gt;go vet&lt;/code&gt; catches code that compiles but is probably wrong. mutest catches tests that pass but probably aren't thorough enough. It's the next layer of quality assurance for your Go project.&lt;/p&gt;

&lt;p&gt;mutest brings mutation testing to Go without the traditional pain points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Focused scope — targets comparison and equality operators, the mutations that catch real bugs&lt;/li&gt;
&lt;li&gt;Fast — compiles once per package, runs many; finishes in seconds, not minutes&lt;/li&gt;
&lt;li&gt;Non-destructive — never touches your source files; uses Go's &lt;code&gt;-overlay&lt;/code&gt; flag&lt;/li&gt;
&lt;li&gt;CI-ready — &lt;code&gt;-diff&lt;/code&gt;, &lt;code&gt;-threshold&lt;/code&gt;, and &lt;code&gt;-json&lt;/code&gt; flags built in&lt;/li&gt;
&lt;li&gt;Zero dependencies — pure Go standard library&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your tests are passing but you're not sure they're actually &lt;em&gt;testing&lt;/em&gt;, give mutest a try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/fchimpan/mutest@latest
mutest ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might be surprised how many mutants survive.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;mutest is open source under the MIT license. Contributions, issues, and stars are welcome at &lt;a href="https://github.com/fchimpan/mutest" rel="noopener noreferrer"&gt;github.com/fchimpan/mutest&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codequality</category>
      <category>go</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
