Mutation Testing in Go: How to Catch What Unit Tests Miss

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Tests Pass, But Bugs Remain?

\n\n

Imagine this: you've written a module in Go, covered it with unit tests at 95% according to go test -cover, all tests are green. You're confident the code works perfectly. But a bug appears in production. Sound familiar?

\n\n

The problem is that the code coverage metric is just the number of lines that were executed. It doesn't tell you what exactly the tests are checking. You can have 100% coverage but never verify that a function returns the correct value, only that it doesn't panic.

\n\n

This is where mutation testing in Go comes into play — a technique that artificially introduces errors into your code and checks whether the tests can detect them. If tests don't fail on mutated code, it means your tests are useless.

\n\n

In this article, we'll break down what mutation testing in Go is, how to set it up, and why it's a must-have for projects where reliability is critical. Let's go!

\n\n

What is Mutation Testing?

\n\n

Mutation testing is a method for evaluating test quality by introducing small changes (mutations) into the program that simulate typical developer errors. The tests are then run. If at least one test fails, the mutation is "killed" — the test works. If all tests pass, the mutation "survived," and that's a red flag: you have a hole in your tests.

\n\n

In Go, mutation testing is often called mutation testing golang, and it's especially useful for verifying business logic, validators, parsers, and other critical components.

\n\n

Example of a Mutation

\n\n

Suppose you have a simple function:

\n\n
func IsEven(n int) bool {\n    return n%2 == 0\n}
\n\n

A typical mutation is replacing == with !=:

\n\n
func IsEven(n int) bool {\n    return n%2 != 0\n}
\n\n

If your test only checks that IsEven(2) doesn't panic, but doesn't check that the result is true, the mutation will survive. The test will pass, even though the function now works exactly opposite to how it should.

\n\n

Why Simple Code Coverage Isn't Enough?

\n\n

Many Go developers are accustomed to relying on go test -cover. It's a useful metric, but it can be deceptive. Consider this example:

\n\n
func Divide(a, b int) (int, error) {\n    if b == 0 {\n        return 0, errors.New("division by zero")\n    }\n    return a / b, nil\n}
\n\n

Test:

\n\n
func TestDivide(t *testing.T) {\n    result, err := Divide(10, 2)\n    if err != nil {\n        t.Fatal(err)\n    }\n    if result != 5 {\n        t.Errorf("expected 5, got %d", result)\n    }\n}
\n\n

Coverage? 100% — all lines were executed. But what if someone accidentally replaces a / b with a * b? The test won't notice, because 10 * 2 = 20, and we expected 5. Will the test fail? No, if we don't check the exact value.

\n\n

Mutation testing in Go will immediately reveal this problem by creating a mutant with multiplication and checking if the test fails.

\n\n

Tools for Mutation Testing in Go

\n\n

The primary tool is go-mutesting. This is a popular library that automatically creates mutations and runs tests. Installation:

\n\n
go get github.com/zimmski/go-mutesting/cmd/go-mutesting
\n\n

There are also alternatives, such as mutesting from the company "Go Test It", but go-mutesting is the de facto standard.

\n\n

How to Run It

\n\n

Basic run:

\n\n
go-mutesting ./...
\n\n

This command will find all _test.go files in the project and check which mutations survive. The result is output to the console with a detailed report.

\n\n

Practical Example: Mutation Testing in Go

\n\n

Let's create a simple calculator package with addition and

Blogs

Book Recommendations