Positive Mutation
how mutation tests will improve your work
ARKADIUSZ KONDAS
Data Scientist
@ Buddy.Works
Code Craftsman
Blogger
Ultra Runner
@ ArkadiuszKondas
github.com/akondas
arkadiuszkondas.com
Agenda:
- Story
- Theory
- Demo
Story
Coders Guild
Systems
Testers Guild
Coverage Report
Who will guard the guards themselves?
Introduction of mutations
Eternal prosperity
Theory
Code Coverage
Measure how thoroughly tests exercise application
Code Coverage
Line Coverage / Statement coverage
Lines: 4/5 Coverage: 80%
Code Coverage
Line Coverage / Statement coverage
Code Coverage
Function and Method Coverage
Code Coverage
Class and Trait Coverage
Code 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;
}
}
Code Coverage
Branch/patch coverage
Code 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.
Code Coverage
Change Risk Anti-Patterns (CRAP) Index
CRAP1(m) = comp(m)^2 * (1 – cov(m)/100)^3 + comp(m)
Code Coverage
Change Risk Anti-Patterns (CRAP) Index
Code Coverage
Metrics
Code Coverage
<?php
declare (strict_types = 1);
class GoldCustomer implements CustomerSpecification
{
/**
* @param Customer $candidate
*
* @return bool
*/
public function isSatisfiedBy(Customer $candidate): bool
{
return $candidate->getTotalPurchases() >= 100;
}
}
Code Coverage
<?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 ...
Code Coverage
/**
* @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]
];
}
Code Coverage
Mutation Testing
to the rescue
Mutation testing
A tool to provide insight in stability of your code
Mutation testing
- run test siut and check if pass
- generate all possible mutations
- for each mutation run testsiut and check if fails
- evaluate results
Mutation testing
example
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
Mutation testing
example
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
Mutation
- killed if at least 1 test fails
- escaped if at all test pass
-
uncovered mutant is not covered
by a test - fatal mutant produces a fatal error
- timeout unit tests exceed allowed timeout
- skipped could not be tested
a piece of code that has been mutated by mutator
Mutators
Binary Arithmetic
Binary Arithmetic
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;
}
Mutators
Metrics
-
Mutation Score Indicator (MSI):
percentage of mutants covered & killed by tests
-
Mutation Code Coverage:
percentage of mutants covered by tests
-
Covered Code MSI:
percentage of killed mutants that werecoverd by tests
Metrics Example
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
Workflow
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
Implementation
- The filesystem
- The Abstract Syntax Tree (AST)
- The bytecode / opcode
Demo
source: https://newsmeter.in/teslas-cybertruck-armour-glass-demo-goes-wrong/
Infection
Eats Code Coverage for breakfast
Infection
- Running all tests for each mutation is inefficient
- Before running tests, gather coverage data from phpunit
- Next only run the tests that cover the mutated code
- Stop testing a mutation as soon as at least 1 test fails
- Sort the test on their execution time
Let's run CLI ...
Summary
Summary
- Write tests
- Separate fast unit tests from slow integration tests
- False positive will show up
- Mutation testing will improve the quality of your tests
Other languages
- Java: Pitest (http://pitest.org/)
- Ruby: Mutant (https://github.com/mbj/mutant)
- C#: NinjaTurtles* (https://ninjaturtles.codeplex.com/)
- JavaScript: Stryker (https://github.com/stryker-mutator/stryker)
-
Python: mutmut
(https://github.com/boxed/mutmut)
Q&A
Resources/attributions
- https://infection.github.io/guide/
- https://doug.codes/php-code-coverage
- https://phpunit.readthedocs.io/en/9.3/code-coverage-analysis.html
- http://crestweb.cs.ucl.ac.uk/resources/mutation_testing_repository/
- https://github.com/mariuszgil/aggregates-by-example
- https://pixabay.com/
- Illustration by Freepik Stories: https://stories.freepik.com/web
Thanks for listening
@ ArkadiuszKondas
github.com/akondas
Pozytywna Mutacja: jak testy mutacyjne usprawnią Twoją pracę
By Arkadiusz Kondas
Pozytywna Mutacja: jak testy mutacyjne usprawnią Twoją pracę
Testowanie mutacyjne to technika pozwalając na pomiar jakości testów. Polega ona na celowym wprowadzaniu małych zmian (mutacji) w kodzie, a następnie sprawdzeniu czy przynajmniej jeden test nie przechodzi. Podczas prezentacji przedstawię koncepcję testów mutacyjnych wraz z praktycznym wdrożeniem na podstawie biblioteki infection oraz interpretacją wyników.
- 1,146