On écrit tous des tests (n’est-ce pas ?), mais comment savoir s’ils sont utiles ?
- Par leur nombre ? Faux, beaucoup de tests ne garantissent pas que l’application fonctionne correctement
- Avec une bonne couverture du code ? Encore faux, mieux mais pas suffisant
L’important est d'être confiant sur la capacité des tests à détecter les problèmes (c’est pourquoi en TDD un test doit échouer au début, pour etre sur qu’il teste bien quelque chose). Laissez-moi donc vous présenter le mutation testing ! Cette technique modifie votre code, lance les tests et s’attend à ce qu’ils échouent. Si non, c’est que cette partie est mal testée… Dans ce talk je détaillerai les principes du mutation testing, expliquerai comment l’utiliser sur un projet scala et montrerai les résultats obtenus sur un projet réel.
10. Loïc Knuchel - @loicknuchel
Solution 2: couverture de code
11. 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("has no assert") {
new Cart(3).add("shoes")
}
12. Loïc Knuchel - @loicknuchel
Code exécuté par des tests != code testé
it("has irrelevant 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
}
}
13. 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é :
● les effets de bords
● la condition limite
● l’ajout dans la liste
it("asserts few things") {
val cart = new Cart(3)
cart.add("shoes")
cart.items.length shouldBe 1
}
14. Loïc Knuchel - @loicknuchel
Code exécuté par des tests != code testé
31. Loïc Knuchel - @loicknuchel
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
32. 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;
}
33. 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);
}
42. 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
Bonjour à tous,
Bienvenu à mon talk sur le mutation testing.
Je me présente, Loïc Knuchel, je suis développeur Scala chez Criteo, j’organise les HumanTalks et j’essaie de faire du bon code…
Je m’intéresse notamment à tous ces sujets, et maintenant aussi au mutation testing
En tant que développeurs, on essaie tous de faire du bon code
Mais on a pas forcément tous la même définition du “bon code”
A minima on peut probablement s’entendre sur le fait que le code doit :
avoir un minimum de bugs
rester lisible pour les autres développeurs
ne pas demander des efforts disproportionnés pour être modifié
Et bien sûr, pour le premier point, on va écrire des tests
Plein de tests
Des tests unitaires qui vont vérifier que chaque morceau de code fait bien ce qu’on attend de lui
Des tests d’intégration qui vont s’assurer que les composants n’aient pas d’incompatibilités
Des tests de bout en bout pour vérifier que l’application est capable d’exécuter les actions voulues par l’utilisateur
Parfois des tests de charge pour prévenir les indisponibilités liées au nombre de requêtes
Voire même des chaos monkey tests pour être certain que même en cas de problème, on puisse conserver un certain service
Et tout ça dans un seul but: avoir une application qui fonctionne au maximum comme attendu
Mais comment savoir si nos tests garantissent vraiment ça ?
On va devoir tester nos tests => testception
Solution 1: le feeling
On connaît bien notre application
On a écrit plein de tests
On a peu de problèmes en prod
Bref, on se dit que ça ne doit pas être mal
Et si on est sérieux avec ça, c’est déjà un très bon point
Solution 2: la couverture de code
Qui connait ?
Ici on lance nos tests et on regarde quels sont les lignes qui ont été exécutées.
ça donne une première idée de la qualité de nos tests
clairement, avec une couverture de code faible, on comprends vite que les parties non couvertes sont à risque car pas du tout exécutées lors des tests
mais la réciproque n’est pas vraie, une bonne couverture de code ne garantit absolument pas des bon tests.
Lorsque le code est exécuté, il n’est pas forcément vérifié => assert manquant ou assert que sur une partie des résultats ou assert non pertinent
c’est pourquoi quand on écrit un test, on doit toujours le faire échouer avant de le faire réussir, mais ça, c’est possible uniquement lorsqu’on le crée, après impossible de s’en assurer
on peut aussi avoir un effet difficilement testable voire invisible du point de vue des tests => ex: logger
dans ce cas il est exécuté du coup il compte dans la couverture de code, mais pas vérifié
Lorsque le code est exécuté, il n’est pas forcément vérifié => assert manquant ou assert que sur une partie des résultats ou assert non pertinent
c’est pourquoi quand on écrit un test, on doit toujours le faire échouer avant de le faire réussir, mais ça, c’est possible uniquement lorsqu’on le crée, après impossible de s’en assurer
on peut aussi avoir un effet difficilement testable voire invisible du point de vue des tests => ex: logger
dans ce cas il est exécuté du coup il compte dans la couverture de code, mais pas vérifié
Lorsque le code est exécuté, il n’est pas forcément vérifié => assert manquant ou assert que sur une partie des résultats ou assert non pertinent
c’est pourquoi quand on écrit un test, on doit toujours le faire échouer avant de le faire réussir, mais ça, c’est possible uniquement lorsqu’on le crée, après impossible de s’en assurer
on peut aussi avoir un effet difficilement testable voire invisible du point de vue des tests => ex: logger
dans ce cas il est exécuté du coup il compte dans la couverture de code, mais pas vérifié
On peut aussi oublier de tester une valeur particulière importante => condition aux limites
Comme toute métrique, elle peut être détournée => introspection pour tout exécuter en un test
et enfin, toutes les lignes de code ne se valent pas, certaines sont beaucoup plus importantes que d’autres et on veut s’assurer que les parties importantes sont très bien testées
la couverture de code donne principalement un chiffre global pas forcément pertinent
La couverture de code, c’est bien mais loin d’être suffisant.
Heureusement, aujourd’hui je vous présente la solution ultime !!!
Ou presque…
Enfin, une meilleure solution que les précédentes en tout cas ;)
Le mutation testing !
Contrairement à ce que son nom pourrait faire penser, c’est pas une nouvelle méthode de test mais une méthode pour évaluer la qualité des tests, un peu comme la couverture de code
D’ailleurs, comme la couverture de code, c’est une technique non-intrusive pour le code de l’application
Il suffit simplement de configurer un outil, qui peut être totalement extérieur au code !
Le mutation testing peut être utilisé dans n’importe quel langage mais chaque langage a son outil spécifique.
Par exemple en Java il y a PIT
Et pour l’utiliser il suffit de l’ajouter au build et de le lancer
C’est aussi simple que ça ! (bien sûr il y a pas mal d’options si on souhaite mais la première intégration est triviale)
En scala il y a scalamu et de la même manière, il suffit de l’ajouter comme plugin et de le lancer
Et enfin, en JavaScript on a stryker
Mais on a toujours pas parlé de ce que fait le mutation testing…
Le principe est très simple
Le framework de mutation testing va créer des mutants et vérifier s’ils sont détectés pour les tests
bien sûr, tout ça est assez coûteux en terme de performances…
Il est important que les tests s’exécutent rapidement et n’aient pas une portée trop large
Et bien sûr il y a quelques optimisations possibles pour améliorer ça