how mutation tests will improve your work
ARKADIUSZ KONDAS
Data Scientist
@ Buddy.Works
Code Craftsman
Blogger
Ultra Runner
@ ArkadiuszKondas
github.com/akondas
arkadiuszkondas.com
Coders Guild
Systems
Testers Guild
Coverage Report
Who will guard the guards themselves?
Introduction of mutations
Eternal prosperity
Measure how thoroughly tests exercise application
Line Coverage / Statement coverage
Lines: 4/5 Coverage: 80%
Line Coverage / Statement coverage
Function and Method Coverage
Class and Trait Coverage
Branch/patch coverage
class AgeVerification
{
public function passes(int $age): bool
{
if($age <= 0) {
throw new \InvalidArgumentException('Age cannot be negative.');
}
return $age >= 18 && $age <= 99;
}
}
Branch/patch coverage
Change Risk Anti-Patterns (CRAP) Index
The Change Risk Anti-Patterns (CRAP) Index is calculated based on the cyclomatic complexity and code coverage of a unit of code. Code that is not too complex and has an adequate test coverage will have a low CRAP index.
The CRAP index can be lowered by writing tests and by refactoring the code to lower its complexity.
Change Risk Anti-Patterns (CRAP) Index
CRAP1(m) = comp(m)^2 * (1 – cov(m)/100)^3 + comp(m)
Change Risk Anti-Patterns (CRAP) Index
Metrics
<?php
declare (strict_types = 1);
class GoldCustomer implements CustomerSpecification
{
/**
* @param Customer $candidate
*
* @return bool
*/
public function isSatisfiedBy(Customer $candidate): bool
{
return $candidate->getTotalPurchases() >= 100;
}
}
<?php
declare (strict_types = 1);
class GoldCustomerTest extends \PHPUnit_Framework_TestCase
{
public function testIsSatisfiedByCustomer()
{
$customer = new Customer();
$goldCustomer = new GoldCustomer();
$this->assertFalse($goldCustomer->isSatisfiedBy($customer));
}
}
100% Code Coverage, but ...
/**
* @param int $totalPurchases
* @param bool $expected
*
* @dataProvider customerProvider
*/
public function testIsSatisfiedReturnsCorrectResult(int $totalPurchases, bool $expected)
{
$customer = new Customer($totalPurchases);
$goldCustomer = new GoldCustomer();
$this->assertEquals($expected, $goldCustomer->isSatisfiedBy($customer));
}
/**
* @return array
*/
public function customerProvider()
{
return [
[1, false],
[99, false],
[100, true],
[200, true]
];
}
Mutation Testing
to the rescue
A tool to provide insight in stability of your code
public function isInRetirementAge() {
return $this->age > 67;
}
function testIsInRetirementAge() {
$this->assertFalse((new Applicant('John', 30))->isInRetirementAge());
$this->assertTrue((new Applicant('John', 70))->isInRetirementAge());
}
public function isInRetirementAge() {
- return $this->age > 67;
+ return $this->age >= 67;
}
The mutant is alive == has escaped
function testIsInRetirementAge() {
- $this->assertFalse((new Applicant('John', 30))->isInRetirementAge());
+ $this->assertFalse((new Applicant('John', 67))->isInRetirementAge());
$this->assertTrue((new Applicant('John', 70))->isInRetirementAge());
}
public function isInRetirementAge() {
- return $this->age > 67;
+ return $this->age >= 67;
}
The mutant is killed
a piece of code that has been mutated by mutator
26) \Humbug\Mutator\Arithmetic\PlusEqual
--- Original
+++ New
@@ @@
for ($n = 0; $n < $this->dimension; ++$n) {
- $centroid->coordinates[$n] += $point->coordinates[$n];
+ $centroid->coordinates[$n] -= $point->coordinates[$n];
}
}
for ($n = 0; $n < $this->dimension; ++$n) {
$this->coordinates[$n] = $centroid->coordinates[$n] / $count;
}
https://github.com/munusphp/munus
506 mutations were generated:
401 mutants were killed
47 mutants were not covered by tests
45 covered mutants were not detected
4 errors were encountered
9 time outs were encountered
0 mutants required more time than configured
Source: Mutation Testing Better Code by Making Bugs Filip van Laenen
CHANGED_FILES=$(git diff origin/master --diff-filter=AM --name-only | grep src/ | paste -sd "," -);
INFECTION_FILTER="--filter=${CHANGED_FILES} --ignore-msi-with-no-mutations";
infection --threads=4 $INFECTION_FILTER
Run mutation testing on changesets
source: https://newsmeter.in/teslas-cybertruck-armour-glass-demo-goes-wrong/
Eats Code Coverage for breakfast
@ ArkadiuszKondas
github.com/akondas