Mutation Testing in C++: When 100% Code Coverage is a Lie
\n\nAre you proud of your 95% code coverage? You might be wasting your time. Code coverage is like a lock that gets checked by just jiggling the handle. Mutation testing is a burglar who actually tries to crack the safe. In this article, we'll break down how mutation testing C++ transforms your tests from a formality into a real weapon. And yes, we won't copy-paste boring definitions from Wikipedia.
\n\nImagine: you wrote a function, covered it with tests, CI is green. Then a bug goes to production. Sound familiar? Most often the problem isn't in the code, but in tests that check the wrong things. Mutation testing for C++ solves exactly this problem — it shows how truly dangerous your tests are to bugs.
\n\nWhat is Mutation Testing and Why Does a C++ Developer Need It?
\n\nMutation testing is a methodology for evaluating test quality by introducing small changes (mutations) into the source code. If tests still pass after a mutation, they are weak. The mutant survived. If tests fail, the mutant is killed, the test works.
\n\nThis is especially relevant for C++ because:
\n- \n
- C++ code often contains complex logic with pointers, templates, and RAII. \n
- Standard coverage metrics (line coverage, branch coverage) are easy to fool. \n
- The cost of a bug in C++ (especially in embedded, finance, game engines) can be catastrophic. \n
A simple example: you have an age check function:
\n\nbool is_adult(int age) {\n return age >= 18;\n}\n\nTest:
\n\nTEST(AdultTest, Positive) {\n EXPECT_TRUE(is_adult(20));\n}\n\nCoverage: 100% of lines. But the mutation age >= 18 → age > 18 will not be detected because 20 still passes. The mutant survived. If you had tests for is_adult(18) and is_adult(17), you would catch the bug. Mutation testing C++ forces you to write such boundary tests.
Main Mutation Operators in C++
\n\nMutation testing tools apply a set of operators (mutators). Here are the most common ones for C++:
\n\n- \n
- Replacing arithmetic operators:
+→-,*→/\n - Replacing logical operators:
&&→||,==→!=\n - Changing boundary conditions:
>=→>,<→<=\n - Deleting expressions: function call replaced with a constant \n
- Changing return value:
return true→return false\n - Loop mutation: changing exit condition or step \n
Example with real code:
\n\nint factorial(int n) {\n if (n <= 1) return 1;\n return n * factorial(n - 1);\n}\n\nMutation #1: n <= 1 → n < 1. If the test calls factorial(1) and expects 1, but the mutation removes the base case — the test will only fail if you check factorial(0) or factorial(1) separately.
Mutation #2: return n * factorial(n - 1) → return n + factorial(n - 1). If tests only use positive numbers, they might not notice the substitution of multiplication for addition.
These are exactly the cases that mutation testing in C++ uncovers.
\n\nTools for Mutation Testing C++
\n\nNot all tools are equally useful. Here are three main players in the C++ market:
\n\n1. Mull
\nModern, active project. Based on LLVM pass. Supports C++17/20, integrates with Google Test, Catch2, doctest