Increase the quality of unit tests using mutation with PITest
Tags: testing, java, mutation, codequality
Published: 2019-07-16 13:30:26



## Introduction

Code coverage is the most common metric to measure code quality, but it does not guarantee that tests are testing the expected behavior.  

> "...100% code coverage score only means that all lines were exercised at least once, but it says nothing about tests accuracy or use-cases completeness, and that’s why mutation testing matters". (Baeldung, 2018)

The idea of mutation testing is to modify the covered code in a simple way, checking whether the existing test set for this code will detect and reject the modifications.

> Good tests should fail when your service rules are changed.

Each change in the code is called a mutant, and it results in an altered version of the program, called a **mutation**. Some types of mutation are:

- Change conditionals Boundary Mutator.

| Original conditional   |      Mutated conditional      | 
|----------|:-------------:|
| <  |  <= | 
| <= |  <  | 
| >  | >=  | 
| >= | >   |
    
- Change mathematical operators.
- Return null instead of Object value.
- And many other types. Check [this documentation](http://pitest.org/quickstart/mutators/) for all available.

Mutations usually react as follows:

- **Killed**: This means the mutant has been **killed** and therefore the part of the code that has been tested is properly covered.

- **Survived**: This means the mutant has **survived**, and the added or changed functionality is not properly covered by tests. 

- **Infinite loop/runtime error**: This usually means that the mutation is something that could not happen in this scenario.

[PITest framework](http://pitest.org/) is a JVM-based mutation testing tool with high performance and easy to use. I do not think this tool has [competitors](http://pitest.org/java_mutation_testing_systems/) who have all of their features.

## Getting started: Step by step with PITest 1.4.5 (2019 released version)

First, we will see how the jacoco code coverage is faulty.

### Create a Demo App 

1 - Go to https://start.spring.io/ and create a simple demo app (without site dependencies).

2 - Edit the `pom.xml` file, add Jacoco and maven plugins:
```xml
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.19.1</version>
            </plugin>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
```

3 - Still in `pom.xml` file, add the unit testing dependencies.

```xml
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.9.0</version>
            <scope>test</scope>
        </dependency>
```

4 - Create a simple service to verify whether a provided input number is between 0 and 100.


![service](https://thepracticaldev.s3.amazonaws.com/i/vlj4jq66wmvkq506oxi8.PNG)


5 - Create a test class (without Asserts) like the one below:

![without_Asserts](https://thepracticaldev.s3.amazonaws.com/i/wvr0meer82goebr1pr1p.PNG)

### Running the Demo App

Run `mvn clean install` in the root directory.

In this step, we can notice that our code is fully covered by unit tests. Open the jacoco report in `target/site/jacoco/index.html`.

![jacoco](https://thepracticaldev.s3.amazonaws.com/i/pelapj63iya15mfmdwki.PNG)
Both line and branch coverage reports 100% unit tests coverage, but nothing is being tested really!

###  Adding the PITest plugin

We can limit code mutation and test runs by using **targetClasses** and **targetTests**. 

And **avoidCallsTo** to keep specified line codes from being mutated. This improves the mutation time.

```xml
             <plugin>
                <groupId>org.pitest</groupId>
                <artifactId>pitest-maven</artifactId>
                <version>1.4.5</version>
                <executions>
                    <execution>
                        <phase>test</phase>
                        <goals>
                            <goal>mutationCoverage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <targetClasses>
                        <param>com.example.demo.service*</param>
                    </targetClasses>
                    <targetTests>
                        <param>com.example.demo.service*</param>
                    </targetTests>
                    <avoidCallsTo>
                        <avoidCallsTo>java.util.logging</avoidCallsTo>
                        <avoidCallsTo>org.apache.log4j</avoidCallsTo>
                        <avoidCallsTo>org.slf4j</avoidCallsTo>
                        <avoidCallsTo>org.apache.commons.logging</avoidCallsTo>
                    </avoidCallsTo>
                </configuration>
            </plugin>
```

### Run the Demo App with PITest

Run `mvn clean install` in the root directory and look at the PITest report in `/target/pit-reports/<date>/index.html`.
Here we can notice the line coverage is still 100% but a new coverage has been introduced: **Mutation Coverage**.

![pit_geral](https://thepracticaldev.s3.amazonaws.com/i/6mkdhnh68mwbix650bip.PNG)


### Adding real tests with assertions

We can add asserts like this:

![test_class2](https://thepracticaldev.s3.amazonaws.com/i/fz0tpnng7m2apfzox1io.PNG)

Run `mvn clean install` and check the PITest report again.

![coverage2](https://thepracticaldev.s3.amazonaws.com/i/cmctusm8dnl234maplvm.PNG)
![coverage2_detail](https://thepracticaldev.s3.amazonaws.com/i/q9s31yi0thutesadth0f.PNG)

PITest executed tests after mutating our original source code and discovered some mutations are not handled by unit tests so we need to fix that.

To do so, we should cover cases including limit test case which means when the provided value is either 0 and 100.

Following are the test cases to cover mutation testing:

```java
 @Test
  public void hundredReturnsTrue() {
    assertThat(cut.isValid(100)).isTrue();
  }

  @Test
  public void zeroReturnsFalse() {
    assertThat(cut.isValid(0)).isFalse();
  }
```
Running again the PITest mutation coverage command and looking at its report, we can now notice both line and mutation coverage look 100% good.

![success](https://thepracticaldev.s3.amazonaws.com/i/nzlrpfuvu46hwnvngj9n.PNG)


## Bonus

We can use the property **mutationThreshold** to define a percentage of mutation at which the build will fail in case this percentage is bellow the threshold.

![fail_build](https://thepracticaldev.s3.amazonaws.com/i/ou19qei2nf0f0ygwz2jr.PNG)


## Performance of PITest in a real scenario

Running PITest in a small project (6300 lines of code) results in:

```
PIT >> INFO : MINION : 3:56:19 PM PIT >> INFO : Checking environment	
PIT >> INFO : MINION : 3:56:20 PM PIT >> INFO : Found  254 tests
================================================================================
- Timings
================================================================================
> scan classpath : < 1 second
> coverage and dependency analysis : 5 seconds
> build mutation tests : < 1 second
> run mutation analysis : 2 minutes and 15 seconds
--------------------------------------------------------------------------------
> Total  : 2 minutes and 21 seconds
--------------------------------------------------------------------------------
================================================================================
- Statistics
================================================================================
>> Generated 733 mutations Killed 690 (94%)
>> Ran 1158 tests (1.58 tests per mutation)

```

For this project, PITest showed that it generated a total of 733 mutations and of this total only 43 survived, resulting in 94% of mutation coverage.

Mutation testing can be a heavy process, but from my experience, by reaching 85% of mutation coverage, my team felt safe enough to make releases without manually testing the product. (That's cool!)

## Conclusion

Note that code coverage is still an important metric, but sometimes it is not enough to guarantee a well-tested code. Mutation testing is a good additional technique to make unit tests better.


{% github silviobuss/pitest-spring-boot2-demo %}


<p><a href="https://twitter.com/silvio_buss"><img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cMkfg_Vv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/gmrz82bjwhej1f1iqb1e.png" alt="" loading="lazy"></a></p>


###  References
https://itnext.io/start-killing-mutants-mutation-test-your-code-3bea71df27f2
https://www.baeldung.com/java-mutation-testing-with-pitest
https://github.com/rdelgatte/pitest-examples