Mutation Testing: The Key to Flawless Code
“Let’s test our tests and catch the bugs before they catch us”
Preface
Imagine a world where every line of code you write not only performs flawlessly but also stands strong against the unexpected. Picture deploying your software with unwavering confidence, knowing it has been crafted with precision and tested with the utmost rigour. Ever wondered if your tests are truly up to the challenge? You’ve written them, and everything seems perfect. But what if there’s a sneaky bug lurking in the shadows? Welcome to the transformative realm of Mutation Testing, which supercharges your testing process. Mutation Testing acts like a detective, uncovering these hidden flaws and ensuring your code is bugproof.
Before moving forward, let’s go through the overview of Mutation Testing to understand the solution better.
Mutation testing, also referred to as code mutation testing, is a type of white box testing that entails modifying certain elements of an application's code to verify whether a software test suite can detect the modifications. The modifications made to the software are designed to induce errors in the program.
The code is mutated by introducing small changes to simulate potential bugs. For example:
These mutations test whether your test cases can detect subtle changes. Learn more about mutators here. The effectiveness of your tests is measured using the mutation score, calculated as:
Mutation score = (Number of killed mutants /Total number of mutants) x 100
At Atlassian, our proactive approach has proven to be highly effective. By implementing mutation testing, we have improved the quality of our tests. In retrospect, we found that better Pitest coverage could have prevented a few incidents, demonstrating its effectiveness in maintaining high standards of code quality.
What is PITest? PITest, often just called PIT, is a clever tool that helps you check how good your unit tests really are. It does this by introducing tiny changes, or mutations, into your code. The aim? To see if your tests can catch these changes. If they do and fail, that’s a win! It means your tests are doing their job. But if they miss the changes and pass, it’s a hint that your tests might need some work.
Without Mutation Testing
You write tests; they test/consume production code that you have written.
With Mutation Testing
You write tests; they test/consume production code that you have written; they also test/consume mutants of the production code that are automatically generated.
How PITest Works: A Simple Walkthrough
In a nutshell, PITest mutates your code and uses your tests to hunt down these changes. This smart process helps you see how effective your tests are at catching mistakes and spots where your test coverage could be better. By focusing on the areas where mutants survive, you can enhance your test suite, leading to stronger and more reliable software.
Fine, but how can I integrate it with my project?
Step 1: Apply the pitest and arcmutate Plugin to your Gradle Project
NOTE: PITest is an open-source tool that provides powerful mutation testing capabilities. The Arcmutate plugin, on the other hand, is the commercial extension of PITest, offering additional features, especially for Kotlin or frameworks like Spring. It’s important to highlight that Arcmutate requires a valid license to use its features. For straightforward Java projects, the standalone PITest plugin may be sufficient and does not require a license.
plugins {
id 'info.solidsoft.pitest' version '1.15.0' //pitest plugin
id "de.undercouch.download" version "5.6.0" // Download plugin
id "com.arcmutate.bitbucket.cloud" version "1.1.1" //arcmutate plugin
[id "com.arcmutate.github.cloud" version "1.1.1" //arcmutate plugin for github users]
}
// The above plugin should be added at the repository level for multi-module project.
Configure the plugin as needed:
pitest {
def base = System.getenv("BASE") ?: 'main'
junit5PluginVersion = '1.2.1'
features.add("+git_mixed(from[$base])", "+auto_threads")
targetClasses = ['com.<project-path>.*']
targetTests = ['com.<project-path>.*']
excludedClasses = ['com.<project-path>.proto.*']
outputFormats = ["XML", "HTML"]
threads = 4
mutationThreshold = 80
verbose = true
avoidCallsTo = []
}
Step 2: Add Plugins for Junit Accelerator, Kotlin, Spring, Git, License Management, and Bitbucket Integration [optional]
To further enhance your project with Git integration, license management, and Bitbucket integration, you can add corresponding plugins:
plugins {
id 'com.arcmutate:pitest-accelerator-junit5:1.1.0'
id 'com.arcmutate:pitest-kotlin-plugin:1.2.2'
id 'com.arcmutate:arcmutate-spring:1.0.0'
id 'com.arcmutate:pitest-git-plugin:1.2.0'
}
// Refer https://docs.arcmutate.com/ for each plugin details
5. License Management: Licenses are plain text files tied to one or more root packages. They will work for any code in those packages until the date on which they expire.
For gradle builds, we suggest using the gradle download task for downloading the latest and greatest version of the license.
领英推荐
tasks.register('arcmutateLicence', Download) {
src 'https://subscriptions.arcmutate.com/AAFFDEMOAAFF/arcmutate-licence.txt'
dest "$buildDir/reports/pitest/arcmutate-licences/arcmutate-licence.txt"
onlyIfModified true
overwrite true
}
To work correctly with gradle’s caching mechanism, the pitest task must depend upon the licence task by adding the following line to build.gradle.
tasks.getByName('pitest').dependsOn('arcmutateLicence')
6. Bitbucket Integration: The com.arcmutate.bitbucket.cloud plugin integrates with Bitbucket cloud and provides two tasks
The pitest-bitbucket task runs pitest against all modules for which it has been configured, then updates the current PR.
The pitest-bitbucket-upload task updates the PR based on previously generated gitci json files.
Parameters can be configured via a pitestBitbucket extension block
pitestBitbucket {
mutantEmoji = ':vomiting_face:'
trailingText = 'Happy mutant hunting'
}
Step 3: Generate the Report
Here you go ?? Execute the command below to generate the report in both HTML and XML formats.
- ./gradlew pitest
Enough Talk, Let’s Do Some Hands-On!
It's time to place your fingers on the keyboard and start coding!
In this section, we'll dive into a practical demo of mutation testing using a simple Calculator class. This example will demonstrate how achieving 100% line coverage doesn’t always mean your tests are effective. By optimizing the test suite, we’ll improve mutation coverage and ensure all mutants are caught. Let’s get started!
Unoptimized Code and Test Suite
Unoptimised Report Analysis
Here’s an example of survived mutants and what they mean:
Optimized Test Suite
After reviewing the mutation report, the test suite is updated to cover edge cases and additional scenarios:
Optimized Report Analysis:
FAQ
Software Development Engineer 2 @Atlassian
1 个月Great article Mahak Tripathi ??
Principal Engineer at Atlassian
1 个月Kudos, Awesome work Mahak Tripathi
Software Engineer II at Atlassian || Former SDE Intern at SourceFuse Technologies || PEC University of Technology
1 个月Fantastic article, Mahak Tripathi Your explanation of mutation testing and the practical example were spot-on. Thanks for sharing these valuable insights!
Software Engineer ll @ Atlassian | C2C Platform
1 个月Great article Mahak Tripathi, Thanks for sharing!
Java Champion, Development Team Coach, Software Quality Specialist, creator of Pitest
1 个月That's a great overview, thanks Mahak. It's worth noting that, in addition to the html report you show in the post, the arcmutate tooling puts the details of surviving mutants directly into pull requests on Github, Bitbucket, Gitlab and Azure.