Mutation Testing in Java: Breaking Code to Make It Better

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

Your Tests Are Lying, and You Don't Even Know It

\n\n

Imagine this: you've written unit tests, code coverage is at 95%. Your manager is happy, the CI/CD pipeline is green. But on Friday evening, a bug slips into production. Sound familiar?

\n\n

The problem is that classic code coverage is an illusion. It shows which lines of code were executed, but it doesn't answer the main question: do the tests actually verify the logic? This is where mutation testing comes into play — a technique that ruthlessly "breaks" your code to check how good your tests really are.

\n\n

In this article, we'll break down what mutation testing in Java is, how it works, and show real examples using the PIT (Pitest) tool. You'll learn how to turn your tests from a "checkbox in a report" into real protection against bugs.

\n\n

What Is Mutation Testing and Why Is It Important?

\n\n

Mutation testing is a method for evaluating test quality where small, deliberately incorrect changes (called mutations) are introduced into the source code. Then your tests are run. If at least one test fails — the mutant is "killed," the test worked. If the tests pass — the mutant "survived," and that's a red flag.

\n\n

A simple metaphor: you're checking how well your alarm system catches thieves. You plant a "thief" (mutant) and see if the alarm (test) goes off. If the thief walks away calmly — the alarm is useless.

\n\n

Why Is Code Coverage a Bad Metric?

\n\n

Consider this code:

\n\n
public boolean isAdult(int age) {\n    if (age >= 18) {\n        return true;\n    }\n    return false;\n}
\n\n

And the test:

\n\n
@Test\nvoid testIsAdult() {\n    assertTrue(isAdult(18));\n}
\n\n

Code coverage will show 100% (both if-else branches executed?). No! Only the true branch was executed. But the test is green. Now imagine someone accidentally changes the condition to age > 21. The test will still pass because 18 > 21 is false, and assertTrue expects true. The bug goes into production.

\n\n

Mutation testing would instantly catch this situation by creating a mutant that changes >= to > or inverts the condition.

\n\n

How Mutation Testing Works: Theory in Simple Terms

\n\n

The process consists of three stages:

\n\n
    \n
  1. Mutant generation. The tool (e.g., PIT) creates multiple versions of your code with small changes: it changes operators, removes method calls, inverts conditions.
  2. \n
  3. Running tests. Your unit tests are run against each mutant.
  4. \n
  5. Analyzing results. If a test fails — the mutant is killed. If it passes — it's alive. The percentage of killed mutants is called the mutation score.
  6. \n
\n\n

A good score: 80-90% killed mutants. Anything lower is a reason to rewrite your tests.

\n\n

Practical Example in Java with PIT

\n\n

Let's take a real example. Suppose we have a service for calculating discounts:

\n\n
public class DiscountService {\n    public double calculateDiscount(double amount, boolean isPremium) {\n        double discount = 0.0;\n\n        if (amount > 100) {\n            discount = 0.1; // 10% discount\n        }\n\n        if (isPremium) {\n            discount += 0.05; // additional 5%\n        }\n\n        // Discount cannot exceed 30%\n        if (discount > 0.3) {\n            discount = 0.3;\n        }\n\n        return discount;\n    }\n}
\n\n

Let's write a naive test:

\n\n
import org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass DiscountServiceTest {\n\n    private final DiscountService service = new                

Blogs

Book Recommendations