1 of 43

Mutation testing

Gotta Kill ’Em All !

Loïc Knuchel

2 of 43

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

3 of 43

Peu de bugs

Loïc Knuchel - @loicknuchel

4 of 43

Loïc Knuchel - @loicknuchel

5 of 43

Stratégies de test

Loïc Knuchel - @loicknuchel

6 of 43

Du code robuste grâce aux tests

Loïc Knuchel - @loicknuchel

7 of 43

Tester les tests

Loïc Knuchel - @loicknuchel

8 of 43

Etape 1: l’intuition

Loïc Knuchel - @loicknuchel

9 of 43

Etape 2: couverture de code

Loïc Knuchel - @loicknuchel

10 of 43

Solution 2: couverture de code

Loïc Knuchel - @loicknuchel

11 of 43

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

12 of 43

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

13 of 43

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é :

  • les effets de bords
  • la condition limite
  • l’ajout dans la liste

it("and a better one") {

val cart = new Cart(3)

cart.add("shoes")

cart.items.length shouldBe 1

}

Loïc Knuchel - @loicknuchel

14 of 43

Code exécuté par des tests != code testé

Loïc Knuchel - @loicknuchel

15 of 43

Tout le code ne se vaut pas

Loïc Knuchel - @loicknuchel

16 of 43

Etape 3

Mutation testing

Loïc Knuchel - @loicknuchel

17 of 43

Génère un mutant

Lance les tests

Vérifie le résultat

Recommence

Loïc Knuchel - @loicknuchel

18 of 43

Mutant tué

Si les tests échouent

Le code muté a été détecté

Il est donc correctement testé

Loïc Knuchel - @loicknuchel

19 of 43

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

20 of 43

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

21 of 43

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

22 of 43

Mutations: opération mathématique

Opérations :

x++x--

+-

*/

%*

Opérations binaires :

&|

^&

>><<

return size + 1;

return size - 1;

Loïc Knuchel - @loicknuchel

23 of 43

Mutations: constantes

Change une constante :

truefalse

01

xx + 1

xnull

Remplace une variable par une constante :

xtrue / false

x0 / 1 / 2

return exists;

if(exists == null)

throw new RuntimeException();

else return null;

Loïc Knuchel - @loicknuchel

24 of 43

Mutations: supprimer une fonction

println(s"item add: $item")

Loïc Knuchel - @loicknuchel

25 of 43

Mutations

Loïc Knuchel - @loicknuchel

26 of 43

En pratique

  • mutants uniquement pour le code couvert par les tests
  • lance uniquement les tests qui couvrent le code muté
  • fonctionne en mode itératif
  • à mettre en priorité pour le code critique
  • activer que les mutations intéressantes

Brute force

Loïc Knuchel - @loicknuchel

27 of 43

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

28 of 43

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

29 of 43

Test 1

Loïc Knuchel - @loicknuchel

30 of 43

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

31 of 43

Tests 1 + 2

Loïc Knuchel - @loicknuchel

32 of 43

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

33 of 43

Tests 1 + 2 + 3

Loïc Knuchel - @loicknuchel

34 of 43

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

35 of 43

Tests 1 + 2 + 3 + 4

Loïc Knuchel - @loicknuchel

36 of 43

Code source

https://github.com/loicknuchel/mutation-testing-sample

Java / Scala / JavaScript / PR acceptées ;)

Loïc Knuchel - @loicknuchel

37 of 43

Loïc Knuchel - @loicknuchel

38 of 43

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

39 of 43

Mise en place: Scala

// project/plugins.sbt

addSbtPlugin("io.github.sugakandrey" % "sbt-scalamu" % "0.1.1")

$ sbt mutationTest

Scalamu

Loïc Knuchel - @loicknuchel

40 of 43

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

41 of 43

Et plein d’autres...

NinjaTurtles pour C#

Mutmut pour Python

mutant pour Ruby

Infection pour PHP

...

Loïc Knuchel - @loicknuchel

42 of 43

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

43 of 43

Questions ?

Loïc Knuchel - @loicknuchel