Mutation testing
Gotta Kill ’Em All !
Loïc Knuchel
Loïc Knuchel
@loicknuchel
Développeur Scala chez
Organisateur des Paris
Software craftsman
loicknuchel@gmail.com
DDD
FP
CQRS
Hexagonal architecture
Property based testing
Event Sourcing
Event Storming
TDD
Living documentation
Peu de bugs
Loïc Knuchel - @loicknuchel
Loïc Knuchel - @loicknuchel
Stratégies de test
Loïc Knuchel - @loicknuchel
Du code robuste grâce aux tests
Loïc Knuchel - @loicknuchel
Tester les tests
Loïc Knuchel - @loicknuchel
Etape 1: l’intuition
Loïc Knuchel - @loicknuchel
Etape 2: couverture de code
Loïc Knuchel - @loicknuchel
Solution 2: couverture de code
Loïc Knuchel - @loicknuchel
Code exécuté par des tests != code testé
class Cart(size: Int) {
val items = mutable.ArrayBuffer[String]()
def add(item: String): Boolean = {
println(s"item add: $item")
val exists = items.contains(item)
if(items.length < size) {
items.append(item)
}
exists
}
}
it("my first test") {
new Cart(3).add("shoes")
}
Loïc Knuchel - @loicknuchel
Code exécuté par des tests != code testé
it("with an assert") {
new Cart(3).add("shoes") shouldBe false
}
class Cart(size: Int) {
val items = mutable.ArrayBuffer[String]()
def add(item: String): Boolean = {
println(s"item add: $item")
val exists = items.contains(item)
if(items.length < size) {
items.append(item)
}
exists
}
}
Loïc Knuchel - @loicknuchel
Code exécuté par des tests != code testé
class Cart(size: Int) {
val items = mutable.ArrayBuffer[String]()
def add(item: String): Boolean = {
println(s"item add: $item")
val exists = items.contains(item)
if(items.length < size) {
items.append(item)
}
exists
}
}
Non testé :
it("and a better one") {
val cart = new Cart(3)
cart.add("shoes")
cart.items.length shouldBe 1
}
Loïc Knuchel - @loicknuchel
Code exécuté par des tests != code testé
Loïc Knuchel - @loicknuchel
Tout le code ne se vaut pas
Loïc Knuchel - @loicknuchel
Etape 3
Mutation testing
Loïc Knuchel - @loicknuchel
Génère un mutant
Lance les tests
Vérifie le résultat
Recommence
Loïc Knuchel - @loicknuchel
Mutant tué
Si les tests échouent
Le code muté a été détecté
Il est donc correctement testé
Loïc Knuchel - @loicknuchel
Mutant vivant
Si les tests réussissent
Le code muté n’a pas été détecté
Les tests sont donc insuffisants
Loïc Knuchel - @loicknuchel
Qu’est-ce qu’un mutant ?
def add(item: String): Boolean = {
println(s"item add: $item")
val exists = items.contains(item)
if (items.length < size) {
items.append(item)
}
exists
}
Code original
def add(item: String): Boolean = {
()
val exists = items.contains(item)
if (items.length < size) {
items.append(item)
}
exists
}
Supprime un appel de fonction
def add(item: String): Boolean = {
println(s"item add: $item")
val exists = items.contains(item)
if (true) {
items.append(item)
}
exists
}
Condition toujours vraie
Loïc Knuchel - @loicknuchel
Mutations: conditions
Modification :
< ⇔ <=
> ⇔ >=
&& ⇔ ||
Constante :
true
false
Inversion :
== ⇔ !=
< ⇔ >=
> ⇔ <=
cond ⇔ !cond
if (items.size() < size) {
items.add(item);
}
if (items.size() <= size) {
items.add(item);
}
Loïc Knuchel - @loicknuchel
Mutations: opération mathématique
Opérations :
x++ ⇔ x--
+ ⇔ -
* ⇔ /
% ⇔ *
Opérations binaires :
& ⇔ |
^ ⇔ &
>> ⇔ <<
return size + 1;
return size - 1;
Loïc Knuchel - @loicknuchel
Mutations: constantes
Change une constante :
true ⇔ false
0 ⇔ 1
x ⇔ x + 1
x ⇔ null
Remplace une variable par une constante :
x ⇔ true / false
x ⇔ 0 / 1 / 2
return exists;
if(exists == null)
throw new RuntimeException();
else return null;
Loïc Knuchel - @loicknuchel
Mutations: supprimer une fonction
println(s"item add: $item")
Loïc Knuchel - @loicknuchel
Mutations
Loïc Knuchel - @loicknuchel
En pratique
Brute force
Loïc Knuchel - @loicknuchel
Exemple
/**
* Take a list of item prices and calculate the bill :
* - if total is higher than 50, apply 10% overall discount
* - if more than 5 items, apply 100% discount on cheapest one
* - if many discount apply, use the higher one
*/
public static Double getPrice(List<Double> prices) {
Double total = sum(prices);
Double discount = 0.0;
if (total >= 50) {
discount = total * 0.1;
}
if (prices.size() >= 5) {
Double minPrice = min(prices);
if (minPrice > discount) {
discount = minPrice;
}
}
return total - discount;
}
Loïc Knuchel - @loicknuchel
Test 1
@Test
public void getPrice_should_be_normal_price_with_few_and_cheap_items() {
assertEquals(24, Demo.getPrice(Arrays.asList(4, 7, 1, 12), 0.001);
}
Loïc Knuchel - @loicknuchel
Test 1
Loïc Knuchel - @loicknuchel
Tests 1 + 2
@Test
public void getPrice_should_be_get_10pc_discound_on_expensive_items() {
assertEquals(54, Demo.getPrice(Arrays.asList(10, 20, 30)), 0.001);
}
Loïc Knuchel - @loicknuchel
Tests 1 + 2
Loïc Knuchel - @loicknuchel
Tests 1 + 2 + 3
@Test
public void getPrice_should_be_get_one_free_item_when_buy_many() {
assertEquals(22, Demo.getPrice(Arrays.asList(3, 5, 2, 8, 1, 4)), 0.001);
}
Loïc Knuchel - @loicknuchel
Tests 1 + 2 + 3
Loïc Knuchel - @loicknuchel
Tests 1 + 2 + 3 + 4
@Test
public void getPrice_should_should_test_boundary_conditions() {
// 50 total value boundary
assertEquals(45, Demo.getPrice(Arrays.asList(5, 10, 15, 20)), 0.001);
// 5 item boundary
assertEquals(43, Demo.getPrice(Arrays.asList(7, 8, 15, 10, 10)), 0.001);
}
Loïc Knuchel - @loicknuchel
Tests 1 + 2 + 3 + 4
Loïc Knuchel - @loicknuchel
Code source
https://github.com/loicknuchel/mutation-testing-sample
Java / Scala / JavaScript / PR acceptées ;)
Loïc Knuchel - @loicknuchel
Loïc Knuchel - @loicknuchel
Mise en place: Java
// pom.xml
<build>
<plugins>
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.2.4</version>
</plugin>
</plugins>
</build>
$ mvn org.pitest:pitest-maven:mutationCoverage
Loïc Knuchel - @loicknuchel
Mise en place: Scala
// project/plugins.sbt
addSbtPlugin("io.github.sugakandrey" % "sbt-scalamu" % "0.1.1")
$ sbt mutationTest
Scalamu
Loïc Knuchel - @loicknuchel
Mise en place: JavaScript
$ ./node_modules/.bin/stryker run
// stryker.conf.js
module.exports = function(config) {
config.set({
testRunner: "jest",
mutator: "javascript",
transpilers: [],
reporter: ["html", "progress"],
coverageAnalysis: "all",
mutate: ["src/**/*.js"]
});
};
$ npm install stryker stryker-api stryker-html-reporter stryker-javascript-mutator stryker-jest-runner --save-dev
Stryker
Loïc Knuchel - @loicknuchel
Et plein d’autres...
Loïc Knuchel - @loicknuchel
Conclusion
Impossible à contourner
Long à exécuter
Couplage code ⇔ tests
Impossible à tuer
Tester ses tests
Meilleure couverture de code !
Facile à mettre en place
Loïc Knuchel - @loicknuchel
Questions ?
Loïc Knuchel - @loicknuchel