Introduction: Tests Pass, But Bugs Remain?
\n\nImagine 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?
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\nThis 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\nIn 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\nWhat is Mutation Testing?
\n\nMutation 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\nIn 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\nExample of a Mutation
\n\nSuppose you have a simple function:
\n\nfunc IsEven(n int) bool {\n return n%2 == 0\n}\n\nA typical mutation is replacing == with !=:
func IsEven(n int) bool {\n return n%2 != 0\n}\n\nIf 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.
Why Simple Code Coverage Isn't Enough?
\n\nMany Go developers are accustomed to relying on go test -cover. It's a useful metric, but it can be deceptive. Consider this example:
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\nTest:
\n\nfunc 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\nCoverage? 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.
Mutation testing in Go will immediately reveal this problem by creating a mutant with multiplication and checking if the test fails.
\n\nTools for Mutation Testing in Go
\n\nThe primary tool is go-mutesting. This is a popular library that automatically creates mutations and runs tests. Installation:
\n\ngo get github.com/zimmski/go-mutesting/cmd/go-mutesting\n\nThere are also alternatives, such as mutesting from the company "Go Test It", but go-mutesting is the de facto standard.
How to Run It
\n\nBasic run:
\n\ngo-mutesting ./...\n\nThis 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.
Practical Example: Mutation Testing in Go
\n\nLet's create a simple calculator package with addition and