go-mutesting [](https://pkg.go.dev/github.com/jonbaldie/go-mutesting/v2) [](https://github.com/jonbaldie/go-mutesting/actions/workflows/mutation.yml) [](LICENSE) [](https://go.dev)

June 16, 2026 · View on GitHub

This repository is being superseded. Development has moved to quality-gates/mutago. This fork will be made read-only once the transition is complete. Please use mutago for new projects.

go-mutesting Go Reference Mutation Testing License: MIT Go 1.26+

go-mutesting is a mutation testing tool for Go. It tweaks your code in small ways and checks whether your tests catch the change. If they don't, that's a gap in your test suite worth closing.

Features

Beyond finding escaped mutants, go-mutesting can enforce quality gates in CI — failing builds below a mutation score threshold, filtering to changed lines only, and ignoring previously-accepted survivors.

FeatureFlag
Quality gates — fail CI below a mutation score--min-msi, --min-covered-msi
Coverage-aware MSI — score covered lines separately--coverage
Baseline file — only fail on new escapes--baseline, --update-baseline
Git diff filter — only mutate changed lines in a PR--git-diff-lines
Per-test filtering — run only covering tests per mutant--per-test
Parallel execution (all CPUs by default)--workers N
LLM-ready escaped-mutant report--logger-agentic-json
GitHub Actions annotations--logger-github
GitLab Code Quality report--logger-gitlab
Compact stats JSON for badges/dashboards--logger-summary-json
Per-mutator allowlist / denylist in configenable_mutators, disable_mutators
Extra flags for every go test call--test-flags
Fine-grained output filter--output-statuses
Quiet mode — suppress killed/skip noise--quiet
Suppress diff output--no-diffs
Dry-run mode — count mutations without running tests--dry-run
Pre-flight check — fail fast if tests already broken--noop
Fail on any escape without a score threshold--fail-on-escaped
Run a single mutant by stable ID--run-mutant-id
Scale per-mutation timeout by baseline run time--timeout-coefficient
Live progress displayautomatic on TTY
go install github.com/jonbaldie/go-mutesting/v2/cmd/go-mutesting@latest

Full documentation: https://jonbaldie.github.io/go-mutesting/

Forked from avito-tech/go-mutesting, itself a fork of zimmski/go-mutesting.

Quick example

The following command mutates the go-mutesting project with all available mutators.

go-mutesting github.com/jonbaldie/go-mutesting/v2/...

For each mutation the tool prints whether the tests caught it. If they didn't, the source code patch is printed so the mutation can be investigated. The following shows an example patch.

for _, d := range opts.Mutator.DisableMutators {
	pattern := strings.HasSuffix(d, "*")

-	if (pattern && strings.HasPrefix(name, d[:len(d)-2])) || (!pattern && name == d) {
+	if (pattern && strings.HasPrefix(name, d[:len(d)-2])) || false {
		continue MUTATOR
	}
}

The example shows that the right term (!pattern && name == d) of the || operator is made irrelevant by substituting it with false. Since this source code change is not detected by the test suite (the tests did not fail), we can mark it as untested code.

Table of contents

What is mutation testing?

The definition of mutation testing is best quoted from Wikipedia:

Mutation testing (or Mutation analysis or Program mutation) is used to design new software tests and evaluate the quality of existing software tests. Mutation testing involves modifying a program in small ways. Each mutated version is called a mutant and tests detect and reject mutants by causing the behavior of the original version to differ from the mutant. This is called killing the mutant. Test suites are measured by the percentage of mutants that they kill. New tests can be designed to kill additional mutants.
-- https://en.wikipedia.org/wiki/Mutation_testing

Tests can be created to verify the correctness of the implementation of a given software system, but the creation of tests still poses the question whether the tests are correct and sufficiently cover the requirements that have originated the implementation.
-- https://en.wikipedia.org/wiki/Mutation_testing

Although the definition focuses on finding code paths not covered by tests, other flaws can be found too. Mutation testing can for example uncover dead and unneeded code.

Mutation testing is also especially interesting for comparing automatically generated test suites with manually written ones.

It is also one of the strongest tools available for keeping AI-generated code honest. AI tools write plausible-looking code that often slips past code review. Mutation testing checks whether your tests would actually catch a bug — not just whether the code looks right.

How do I use go-mutesting?

Install the binary with go install:

go install -v github.com/jonbaldie/go-mutesting/v2/...

The binary's help can be invoked by executing the binary without arguments or with the --help argument.

go-mutesting --help

Note: This README describes only a few of the available arguments. It is therefore advisable to examine the output of the --help argument.

The targets of the mutation testing can be defined as arguments to the binary. Every target can be either a Go source file, a directory or a package. Directories and packages can also include the ... wildcard pattern which will search recursively for Go source files. Test source files with the suffix _test are excluded, since this would interfere with the testing process most of the time.

The following example gathers all Go files from the given targets and generates mutations with all available mutators.

go-mutesting parse.go example/ github.com/jonbaldie/go-mutesting/v2/mutator/...

Every mutation has to be tested using an exec command. By default the built-in exec command is used, which tests a mutation using the following steps:

  • Replace the original file with the mutation.
  • Execute all tests of the package of the mutated file.
  • Report if the mutation was killed.

Alternatively the --exec argument can be used to invoke an external exec command. The /scripts/exec directory holds basic exec commands for Go projects. The test-mutated-package.sh script implements all steps and almost all features of the built-in exec command. It can be for example used to test the github.com/jonbaldie/go-mutesting/v2/example package.

go-mutesting --exec "$GOPATH/src/github.com/jonbaldie/go-mutesting/scripts/exec/test-mutated-package.sh" github.com/jonbaldie/go-mutesting/v2/example

The execution will print the following output.

KILLED example/example.go:18 (statement/remove)
KILLED example/example.go:22 (branch/if)
KILLED example/example.go:24 (numbers/incrementer)
--- Original
+++ New
@@ -16,7 +16,7 @@
        }

        if n < 0 {
-               n = 0
+
        }

        n++
ESCAPED example/example.go:17 (statement/remove)
KILLED example/example.go:26 (arithmetic/base)
KILLED example/example.go:28 (expression/remove)
--- Original
+++ New
@@ -24,7 +24,6 @@
        n += bar()

        bar()
-       bar()

        return n
 }
ESCAPED example/example.go:25 (statement/remove)
KILLED example/example.go:30 (branch/if)
The mutation score is 75.00% (6 killed, 2 escaped, 0 errored, 0 not covered, 0 skipped, 8 total)
The covered-code mutation score is 0.00%

The output shows eight mutations. Six were killed (tests detected the mutation — shown as KILLED). Two escaped (tests didn't catch them — shown as ESCAPED), and their diffs are printed so you can write a test to cover the gap.

The summary shows the mutation score (MSI): killed / total. For the example above, 6/8 = 75.00%. A score of 100% means every mutation was caught.

Blacklist false positives

Mutation testing can produce false positives when the mutated code path is never reachable, or when the unoptimized path produces the same result as the optimized one. These cases are not bugs in your tests — they just aren't worth tracking down.

Use --blacklist with a file that lists the MD5 checksum of each mutation to ignore (one per line). Checksums are derived from only the lines that actually changed, not the whole file, so they stay valid when unrelated code in the same file is edited.

To get the checksum for a mutation, run go-mutesting with --debug and copy the hex string printed next to the mutation. For example, if a mutation's checksum is a1b2c3d4..., create a file:

a1b2c3d4e5f6...

The blacklist file, which is named example.blacklist in this example, can then be used to invoke go-mutesting.

go-mutesting --blacklist example.blacklist github.com/jonbaldie/go-mutesting/v2/example

The execution will print the following output.

KILLED example/example.go:18 (statement/remove)
KILLED example/example.go:22 (branch/if)
KILLED example/example.go:24 (numbers/incrementer)
--- Original
+++ New
@@ -16,7 +16,7 @@
        }

        if n < 0 {
-               n = 0
+
        }

        n++
ESCAPED example/example.go:17 (statement/remove)
KILLED example/example.go:26 (arithmetic/base)
KILLED example/example.go:28 (expression/remove)
KILLED example/example.go:30 (branch/if)
The mutation score is 85.71% (6 killed, 1 escaped, 0 errored, 0 not covered, 0 skipped, 7 total)
The covered-code mutation score is 0.00%

By comparing this output to the original output we can state that we now have 7 mutations instead of 8.

Skipping make() arguments mutation

Before this filter, numeric arguments in make() calls for slices and maps were mutated by incrementer/decrementer mutators, leading to false positives or invalid code:

// Original code
slice := make([]int, 0)  // Capacity argument (0) was mutated

// Mutated versions
slice := make([]int, 1)  // Incrementer mutation
slice := make([]int, -1)   // Decrementer mutation

These mutations are almost always irrelevant because:

  1. They don't affect logical correctness
  2. Capacity/length arguments are typically intentional
  3. Tests rarely validate exact allocation sizes

The filter prevents mutations in make() arguments.

Quality gates

Use --min-msi and --min-covered-msi to fail CI if mutation scores drop below a threshold. The tool exits with code 4 when a gate isn't met.

go-mutesting --min-msi 60 --min-covered-msi 80 ./...

Add --coverage to generate a coverage profile first. Mutants on uncovered lines are marked "not covered" and excluded from the covered-MSI denominator (so you're not penalised for code your tests don't reach at all).

go-mutesting --coverage --min-msi 50 --min-covered-msi 75 ./...

The final summary includes a per-mutator breakdown so you can see which mutation types your tests are weakest against.

Use --noop to run the test suite once without any mutations first. If the clean suite already fails, go-mutesting exits immediately rather than producing meaningless results.

Use --timeout-coefficient to scale the per-mutation timeout relative to the baseline test-suite run time (e.g. --timeout-coefficient 3 allows each mutation up to 3× the clean run). More reliable than a fixed --exec-timeout on machines with variable load.

Git diff filtering (CI mode)

--git-diff-lines limits mutation to lines changed since a given git ref. The comparison is made against the merge-base of that ref and your current branch, so it mutates exactly the lines a pull request shows — even when your branch is behind the target. Commits that landed on the target branch after you branched off are not attributed to your work. Combine it with --ignore-msi-with-no-mutations so the gate passes cleanly on PRs that touch no mutable code.

go-mutesting \
  --git-diff-lines \
  --git-diff-base master \
  --ignore-msi-with-no-mutations \
  --min-msi 80 \
  ./...

Add --logger-github to emit escaped mutants as GitHub Actions ::warning annotations, so they show up inline on the PR diff.

Baseline — only fail on new escapes

If you have existing mutants that survive and your team has accepted them, you can record them in a baseline file. Future runs only fail when a new mutant escapes — not an already-known one.

# First run: record the current survivors and exit 0
go-mutesting --update-baseline ./...

# Normal CI run: fail only if something new escapes
go-mutesting --fail-on-escaped --baseline go-mutesting-baseline.json ./...

Commit go-mutesting-baseline.json to your repo. The baseline uses stable mutant IDs — they survive refactors that shift line numbers without changing the actual code.

LLM-ready report

--logger-agentic-json writes go-mutesting-agentic.json. Each escaped mutant gets a stable ID, the diff, surrounding context lines, nearby test file paths, a plain-English description of what the mutator did, and a hint for writing a killing test. Feed it to an LLM to get targeted test suggestions.

go-mutesting --logger-agentic-json --quiet ./...

Use --run-mutant-id to re-run a single mutant by its stable ID (copy the id field from go-mutesting-agentic.json). Useful for iterating on a specific test gap without waiting for the full suite.

Live progress

When running in a terminal, go-mutesting shows a live progress line on stderr (killed / escaped / skip counts). It clears automatically before the final summary. It is suppressed in --verbose, --debug, and silent mode.

CI integration (GitHub Actions)

A minimal workflow that gates on mutation score. Adjust thresholds to match your project's baseline.

name: Mutation Testing
on:
  push:
    branches: [master]
  pull_request:

jobs:
  mutating:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-go@v6
        with:
          go-version: stable
      - run: go build -o /tmp/go-mutesting ./cmd/go-mutesting
      - run: |
          /tmp/go-mutesting \
            --coverage \
            --min-msi 70 \
            --min-covered-msi 80 \
            --logger-github \
            ./...

--logger-github emits escaped mutants as ::warning annotations that appear inline on the PR diff. --coverage excludes untested code from the covered-MSI denominator so you aren't penalised for dead code your tests never reach.

For GitLab, replace --logger-github with --logger-gitlab. This writes go-mutesting-gitlab.json in GitLab Code Quality format, which GitLab CI picks up as a code quality report on merge requests.

Adopting gates on a legacy codebase: record the current survivors first, then only fail on new regressions.

# Run once to capture current state
go-mutesting --update-baseline ./...
git add go-mutesting-baseline.json
git commit -m "chore: establish mutation baseline"

# CI: only fail if something new escapes
go-mutesting --baseline go-mutesting-baseline.json --fail-on-escaped ./...

JSON output schemas

--logger-summary-json

Writes go-mutesting-summary.json after each run. Useful for badges, dashboards, and downstream scripts.

{
  "totalMutantsCount": 42,
  "killedCount": 35,
  "escapedCount": 5,
  "errorCount": 0,
  "skippedCount": 2,
  "notCoveredCount": 0,
  "msi": 0.8333,
  "coveredCodeMsi": 0.9211
}
FieldTypeDescription
totalMutantsCountintTotal mutations generated
killedCountintMutations caught by tests
escapedCountintMutations not caught (test gaps)
errorCountintMutations that caused a build or test error
skippedCountintMutations skipped (blacklisted or annotated)
notCoveredCountintMutations on lines with no coverage (requires --coverage)
msifloatMutation Score Indicator: killed / total, range 0–1
coveredCodeMsifloatMSI restricted to covered lines, range 0–1

--logger-agentic-json

Writes go-mutesting-agentic.json — a richer payload designed for LLM consumption. Each survived mutant gets a stable ID, the unified diff, surrounding context, nearby test file paths, a plain-English description, and a hint for writing a killing test.

{
  "generated_at": "2026-05-19T08:13:38Z",
  "msi": 0.5857,
  "escaped_count": 5,
  "reminder": "A mutant is an example of how this code could be wrong...",
  "mutants": [
    {
      "id": "abc123",
      "file": "pkg/foo/foo.go",
      "line": 42,
      "mutator": "branch/if",
      "diff": "--- Original\n+++ Mutated\n...",
      "context_start_line": 39,
      "context_lines": ["func Foo() {", "  if x > 0 {", "  }"],
      "test_files": ["pkg/foo/foo_test.go"],
      "description": "Removes an if-block body so the condition becomes a no-op",
      "kill_hint": "Write a test that enters this branch and asserts the output or side effect it produces"
    }
  ]
}
FieldTypeDescription
generated_atstringRFC 3339 timestamp of the run
msifloatOverall MSI as a percentage (0–100) — note: summary JSON uses 0–1 ratio
escaped_countintNumber of survived mutants
reminderstringPlain-English reminder about how to interpret mutants; included as context for LLMs
mutants[].idstringStable hash of file + mutator + diff — survives refactors
mutants[].filestringPath to the mutated file, relative to the module root
mutants[].lineintLine number of the mutation
mutants[].mutatorstringMutator name (e.g. branch/if)
mutants[].diffstringUnified diff of original vs mutated
mutants[].context_start_lineint1-based line number of context_lines[0]; anchors the snippet without guessing
mutants[].context_lines[]stringSurrounding source lines for orientation
mutants[].test_files[]stringTest files in the same package
mutants[].descriptionstringHuman-readable description of what the mutator changed
mutants[].kill_hintstringConcrete suggestion for a test that would kill this mutant

Feed go-mutesting-agentic.json to an LLM to get targeted test suggestions for each gap.

Mutation control via annotations

To further reduce false positives and provide granular control over mutations, go-mutesting now supports special comment annotations. These allow you to exclude specific functions, lines, or patterns from mutation.

Annotation Types

  1. // mutator-disable-func — disables all mutations in the function that follows it.
// mutator-disable-func
func CalculateDiscount(price float64) float64 {
    return price * 0.9
}
  1. // mutator-disable-next-line <mutator1>, <mutator2> — disables mutations on the next line. Use * for all mutators.
// mutator-disable-next-line *
x = 42

// mutator-disable-next-line branch/if, increment
if x > 0 {
    y += 1
}
  1. // mutator-disable-regexp <pattern> <mutator1>, <mutator2> — disables mutations on any line in the file matching the regex. Use * for all mutators.
s := MyStruct{name: "Go"}
s.Method()

// mutator-disable-regexp s\.Method\(\) *

All mutation annotations only apply to the file where they are declared. There is no global/cross-file propagation.

How do I write my own mutation exec commands?

A mutation exec command is invoked for each mutation. Commands should handle at least the following phases.

  1. Setup the source to include the mutation.
  2. Test the source by invoking the test suite and possibly other test functionality.
  3. Cleanup all changes and remove all temporary assets.
  4. Report if the mutation was killed.

It is important to note that each invocation should be isolated and therefore stateless. This means that an invocation must not interfere with other invocations.

A set of environment variables, which define exactly one mutation, is passed on to the command.

NameDescription
MUTATE_CHANGEDDefines the filename to the mutation of the original file.
MUTATE_DEBUGDefines if debugging output should be printed.
MUTATE_ORIGINALDefines the filename to the original file which was mutated.
MUTATE_PACKAGEDefines the import path of the original file.
MUTATE_TIMEOUTDefines a timeout which should be taken into account by the exec command.
MUTATE_VERBOSEDefines if verbose output should be printed.
TEST_RECURSIVEDefines if tests should be run recursively.

A command must exit with an appropriate exit code.

Exit codeDescription
0The mutation was killed. Which means that the test led to a failed test after the mutation was applied.
1The mutation is alive. Which means that this could be a flaw in the test suite or even in the implementation.
2The mutation was skipped, since there are other problems e.g. compilation errors.
>2The mutation produced an unknown exit code which might be a flaw in the exec command.

Examples for exec commands can be found in the scripts directory.

Which mutators are implemented?

Arithmetic mutators

arithmetic/base

NameOriginalMutated
Plus+-
Minus-+
Multiplication*/
Division/*
Modulus%*

arithmetic/bitwise

NameOriginalMutated
BitwiseAnd&|
BitwiseOr|&
BitwiseXor^&
BitwiseAndNot&^&
ShiftRight>><<
ShiftLeft<<>>

arithmetic/assign_invert

NameOriginalMutated
AddAssign+=-=
SubAssign-=+=
MulAssign*=/=
QuoAssign/=*=
RemAssign%=*=
AndAssign&=|=
OrAssign|=&=
XorAssign^=&=
ShlAssign<<=>>=
ShrAssign>>=<<=
AndNotAssign&^=&=

arithmetic/assignment

NameOriginalMutated
AddAssignment+==
SubAssignment-==
MulAssignment*==
QuoAssignment/==
RemAssignment%==
AndAssignment&==
OrAssignment|==
XorAssignment^==
SHLAssignment<<==
SHRAssignment>>==
AndNotAssignment&^==

arithmetic/negate

Inverts unary minus expressions. Catches code that relies on a sign flip that tests don't verify.

NameOriginalMutated
InvertNegation-x+x

Loop mutators

loop/break

NameOriginalMutated
Breakbreakcontinue
Continuecontinuebreak

loop/condition

NameOriginalMutated
for k < 100k < 1001 < 1
for i := 0; i < 5; i++i < 51 < 1

loop/range_break

It is a loop/condition-like mutator in its purpose: removing iterations from code.
However, the implementation is slightly different. The mutator adds a break to the beginning of each range loop.

NameOriginal BodyMutated Body
for i,v := range xwithout breakwith break

Numbers mutators

numbers/incrementer

NameOriginalMutated
IncrementInteger100101
IncrementFloat10.111.1

numbers/decrementer

NameOriginalMutated
DecrementInteger10099
DecrementFloat10.19.1

numbers/float-negate

Replaces a float literal with its negation. Catches missing sign-handling in arithmetic.

NameOriginalMutated
FloatNegate3.14-3.14

Composite mutators

composite/field-clear

Drops one keyed field from a composite literal (struct, map, or keyed array/slice literal), letting it fall back to its zero value. Targets fields that are set to a meaningful value but never asserted by a test — e.g. an options or config struct populated in full where only one or two fields actually matter to the suite. Fields already set to a zero value (0, "", false, nil) and positional (unkeyed) elements are skipped to avoid no-op mutations.

NameOriginalMutated
FieldClearConfig{Timeout: 30, Retries: 3}Config{Retries: 3}

Concurrency mutators

concurrency/goroutine-remove

Removes the go keyword from goroutine launches, turning concurrent calls into synchronous ones. Kills tests that rely on goroutines running independently.

NameOriginalMutated
GoroutineRemovego f()f()

Select mutators

select/case-remove

Empties the body of each case branch in a select statement, one at a time.

select/default-remove

Empties the default branch of a select statement.

Conditional mutators

conditional/negated

NameOriginalMutated
GreaterThanNegation><=
LessThanNegation<>=
GreaterThanOrEqualToNegation>=<
LessThanOrEqualToNegation<=>
Equal==!=
NotEqual!===

If you are looking for simple comparison mutators - see expression-mutators

conditional/bool-literal

Swaps truefalse in assignment right-hand sides and function call arguments. Finds hardcoded boolean values that tests never flip — e.g. a config flag that always stays at its default.

NameOriginalMutated
BoolLiteralx = truex = false

conditional/not

Removes the ! operator from negated conditions in if, for, and &&/|| expressions. Finds negations that tests never exercise the non-negated path of.

NameOriginalMutated
ConditionalNotif !x { ... }if x { ... }

Branch mutators

branch/case

Empties case bodies.

branch/if

Empties branches of if and else if statements.

branch/else

Empties branches of else statements.

Expression mutators

expression/comparison

Searches for comparison operators, such as > and <=, and replaces them with similar operators to catch off-by-one errors, e.g. > is replaced by >=.

NameOriginalMutated
GreaterThan>>=
LessThan<<=
GreaterThanOrEqualTo>=>
LessThanOrEqualTo<=<

expression/logical

Swaps && and || operators.

NameOriginalMutated
LogicalAnd&&||
LogicalOr||&&

expression/remove

Searches for && and || operators and makes each term of the operator irrelevant by using true or false as replacements.

expression/context-nil

Replaces context.Context arguments at call sites with nil. Finds code paths that silently ignore a nil context instead of propagating it.

NameOriginalMutated
ContextNilf(ctx, x)f(nil, x)

expression/error-guard

Replaces the condition of if err != nil and if err == nil guards with a boolean constant. Finds error-handling branches that tests never exercise.

NameOriginalMutated
ErrNotNilif err != nilif false
ErrIsNilif err == nilif true

expression/errorf-wrap

Downgrades the error-wrapping verb in Errorf-style calls from %w to %v. The message is byte-for-byte identical, but the returned error no longer wraps its cause, so errors.Is and errors.As against the original error stop matching. Finds code that wraps errors out of habit where no test ever unwraps the result.

NameOriginalMutated
ErrorfWrapfmt.Errorf("load: %w", err)fmt.Errorf("load: %v", err)

expression/recover-clear

Neutralises a recover() call by turning it into any(nil). Because both expressions have type any, the rewrite type-checks in every context, but the recovered value is always nil, so the guarded recovery branch never runs and a panic propagates. Finds deferred recovery blocks that no test exercises.

NameOriginalMutated
RecoverClearif r := recover(); r != nilif r := any(nil); r != nil

expression/string-literal

Replaces non-empty string literals in == and != comparisons with "". Finds code that compares against a specific string value that tests never assert on — e.g. if err.Error() == "not found" where an empty-string match would still pass.

NameOriginalMutated
StringLiterals == "expected"s == ""

Statement mutators

statement/remove

Removes assignment, increment, decrement and expression statements.

statement/remove-self-assign

Removes self-assignment statements (a = a). These are typically dead code; this mutator confirms the surrounding tests don't accidentally rely on them.

statement/return

Replaces each return value with the zero value for its type (false for bool, 0 for int, "" for string, nil for pointers and interfaces). Uses go/types for type resolution. Finds functions whose return values tests never validate.

statement/defer-remove

Removes the defer keyword, turning deferred calls into immediate calls. Tests whether the timing of cleanup matters — e.g. mutex unlocks and file closes that must happen after the function body, not during it.

NameOriginalMutated
DeferRemovedefer f()f()

Config file

There is a configuration file where you can fine-tune mutation testing.
The config must be written in YAML format.
If --config is provided, go-mutesting will use that file. Otherwise no config file is used.
The config contains the following parameters:

NameDefault valueDescription
skip_without_testtrueSkip files without _test.go tests.
skip_with_build_tagstrueSkip test files that contain build constraints.
json_outputfalseWrites a report.json file with the mutation test report.
html_outputfalseWrites a go-mutesting-report.html file with the mutation test report.
silent_modefalseDo not print mutation stats.
min_msi0Minimum required MSI (0–100). 0 means no gate.
min_covered_msi0Minimum required covered-code MSI (0–100). 0 means no gate.
exclude_dirs[]string(nil)File path prefixes to skip. Any file whose path starts with one of these strings is excluded. vendor/ skips all files under vendor; internal/generated skips any path starting with that string.
disable_mutators[]string(nil)Mutator names to disable via config. Merged with --disable CLI flags. Supports trailing-* wildcard (e.g. arithmetic/*).
enable_mutators[]string(nil)Allowlist: if non-empty, only matching mutators run. --disable can still exclude entries. Supports trailing-* wildcard.
ignore_source_lines[]string(nil)List of regexes. Any source line matching one of these patterns is skipped entirely. Useful for suppressing mutations on generated code or boilerplate.

Example config file:

skip_without_test: true
min_msi: 70
min_covered_msi: 80
exclude_dirs:
  - vendor/
  - internal/generated
disable_mutators:
  - numbers/incrementer
  - numbers/decrementer
ignore_source_lines:
  - "// Code generated"
  - "nolint"

How do I write my own mutators?

Each mutator must implement the Mutator interface of the github.com/jonbaldie/go-mutesting/v2/mutator package. The methods of the interface are described in detail in the source code documentation.

Additionally each mutator has to be registered with the Register function of the github.com/jonbaldie/go-mutesting/v2/mutator package to make it usable by the binary.

Examples for mutators can be found in the github.com/jonbaldie/go-mutesting/v2/mutator package and its sub-packages.

Other mutation testing tools for Go

The main active alternative is gremlins. This project is forked from avito-tech/go-mutesting, which has been inactive since late 2025.

gremlins is well-maintained and simple to use. It has a clean CLI and a solid set of mutators. It does not have MSI quality gates, a baseline file, coverage-aware filtering, or a git-diff mode, so it works well for local exploration but is harder to wire into CI in a way that fails only on new regressions.

avito-tech/go-mutesting is the dormant upstream this project was forked from. This fork adds everything in the features table above.

If you want the smallest possible tool to run locally and see which mutants survive, gremlins is a reasonable choice. If you want to enforce mutation score thresholds in CI, track accepted escapes in a baseline, or pipe results to an LLM for test suggestions, this tool is the better fit.

Can I make feature requests and report bugs and problems?

Sure, just submit an issue via the project tracker and I'll see what I can do.