Mutation testing in Go
Now that we’re done yak shaving, we can start talking about mutation testing. As an engineer at Google, I often use the Go programming language (which I really enjoy), so that is my choice for these examples; however, mutation testing is available for other languages.
Constructing Bolson people
Let’s start with an example; we have a people
package, where a person has an age and a name. For these people to be appropriate for our quest, they need to be over 18, have names with at least two whitespace-separated words in them, and have those names end with -son. You can claim those are the strangest software project requirements you’ve ever had all you want, I know better.
|
|
Now, validatePerson
performs the overall validation, but we’ve split it into smaller check*
functions to make them simple to test independently, in case the requirements get more complicated in the future. Here are the tests:
|
|
Running go test -cover
will show us that we have 100% test coverage! Hurray! However, danger lurks. In a couple of months, a newcomer to the team will refactor validatePerson
to add logging indicating why a person is considered invalid, all the tests will pass… and suddenly one “Christian Eriksen” is counted by the system as valid. How can this be? All the tests still pass, and we had 100% coverage!
Using mutation testing
Let’s see if mutation testing can help us out. I put my code in $GOPATH/src/github.com/lutzky/people
, so I install and run zimmski/go-mutesting
:
|
|
|
|
|
|
What the mutation testing package does is take the test-covered code (all of people.go
, in our case) and attempt to modify it at random, so that it will still build, but the logic will change; things like removing statements, changing conditions in if
statements, or in this case - changing an arbitrary boolean value to true
. If the code is correct and tested properly, any such mutated version of the code (“mutant”) should not pass the tests (the tests should “kill the mutant”).
In this case, it appears that modifying checkBolsonPolicy(p)
to true
(which is the same as just removing it and the preceding &&
) does not cause any tests to fail. Indeed, in TestValidPerson
, none of the test cases violate the Bolson policy! If we try adding a test case person{"Bob Rasmussen", 15}
this mutant would still survive, as checkAge(p)
would return false; so we have to make sure checkBolsonPolicy
on its own is sufficient to identify this test case as invalid. Indeed, adding person{"Bob Rasmussen", 19}
to the test cases for TestValidPerson
gets a mutation score of 1.0, fixing our problem.
Drawbacks
Mutation testing can sometimes be noisy. For example, if we write validatePerson
like so:
|
|
…then the following mutant would survive:
|
|
I would treat this mutant possibility as very “meh”. So much like you shouldn’t necessarily fail your build if coverage is less than 100%, you probably shouldn’t fail your build if the mutation score is less than 1.0, and quite likely not based on the mutation score at all. It would help if there were a way to annotate lines as “do not mutate”. While zimmski/go-mutesting does support blacklisting of specific mutants, these blacklists are based on the checksum of the mutated code, which would have to be updated every time the tested code changes.
Happy testing!