Couverture et mutation testing¶
Mesurer la qualité des tests — métriques de couverture, leurs limites et le mutation testing comme complement.
Métriques de couverture¶
La couverture de code mesure le pourcentage de code exécuté lors des tests. C'est la métrique la plus repandue — et la plus mal comprise.
Types de couverture¶
| Type | Ce qui est mesure | Précision |
|---|---|---|
| Line | Pourcentage de lignes exécutées | Faible |
| Branch | Pourcentage de branches if/else exécutées | Moyenne |
| Condition | Chaque sous-condition booleenne évaluée true et false | Élevée |
| Path | Chaque chemin d'exécution possible | Très élevée |
Exemple¶
def categorize(age):
if age < 0:
return "invalide"
elif age < 18:
return "mineur"
else:
return "majeur"
# Ce test donne 66% de couverture branch (2 branches sur 3)
def test_categorize():
assert categorize(25) == "majeur"
assert categorize(10) == "mineur"
# Manque : age < 0
Les limites de la couverture¶
100% de couverture ≠ code bien teste
La couverture mesure le code exécuté, pas le code vérifié. Un test sans assertion atteint 100% de couverture en exécutant tout sans rien vérifier.
# 100% de couverture, 0% de valeur
def test_sort_useless():
sort([3, 1, 2]) # execute le code mais ne verifie rien
Les faux sentiments de sécurité¶
| Situation | Couverture | Code bien teste ? |
|---|---|---|
| Tests avec assertions précises | 80% | Oui |
| Tests sans assertions | 100% | Non |
| Tests qui verifient l'implémentation | 90% | Fragile |
| Tests qui ignorent les cas limites | 85% | Partiellement |
Quel objectif de couverture ?¶
| Contexte | Objectif raisonnable |
|---|---|
| Code métier critique | 80-90% branch |
| Code d'infrastructure | 60-70% |
| Code généré / boilerplate | Pas pertinent |
| Prototype | 0% (temporaire) |
La couverture est un signal, pas un objectif
Utilisez la couverture pour détecter le code non teste (les zones a 0%), pas pour atteindre un pourcentage arbitraire. Passer de 80% a 85% coute souvent plus cher que la valeur ajoutee.
Mutation testing¶
Le mutation testing répond à la question que la couverture ne pose pas : "mes tests detectent-ils vraiment les bugs ?"
Le principe¶
- Le framework crée des mutants — des copies du code avec une petite modification (mutation)
- Les tests sont exécutés sur chaque mutant
- Si les tests échouent → le mutant est tue (bien)
- Si les tests passent → le mutant survit (les tests sont insuffisants)
graph TD
A["Code original"] --> B["Generer des mutants"]
B --> M1["Mutant 1 :<br/>if age < 18 → if age <= 18"]
B --> M2["Mutant 2 :<br/>return 'majeur' → return 'mineur'"]
B --> M3["Mutant 3 :<br/>if age < 0 → if age < 1"]
M1 --> T1["Executer les tests"]
M2 --> T2["Executer les tests"]
M3 --> T3["Executer les tests"]
T1 -->|"tests echouent"| K1["Mutant tue ✓"]
T2 -->|"tests echouent"| K2["Mutant tue ✓"]
T3 -->|"tests passent"| S3["Mutant survit ✗"] Types de mutations¶
| Mutation | Exemple |
|---|---|
| Opérateur relationnel | < → <=, == → != |
| Opérateur arithmetique | + → -, * → / |
| Valeur de retour | return true → return false |
| Suppression d'instruction | Supprimer une ligne de code |
| Negation de condition | if (x) → if (!x) |
| Valeur constante | 0 → 1, "" → "mutant" |
Interpretation du score¶
| Score | Interpretation |
|---|---|
| > 80% | Tests solides — la plupart des bugs sont détectés |
| 60-80% | Correct — des zones a renforcer |
| < 60% | Tests faibles — beaucoup de bugs passeraient inapercus |
Outils de couverture¶
Python — coverage.py¶
# Executer les tests avec couverture
pytest --cov=src --cov-report=html --cov-branch
# Voir le rapport
open htmlcov/index.html
JavaScript — c8 / Istanbul¶
Configuration de seuils (quality gates)¶
# pyproject.toml
[tool.coverage.report]
fail_under = 80
show_missing = true
[tool.coverage.run]
branch = true
// jest.config.js
{
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80
}
}
}
Outils de mutation testing¶
Python — mutmut¶
pip install mutmut
# Lancer le mutation testing
mutmut run --paths-to-mutate=src/
# Voir les resultats
mutmut results
# Voir un mutant survivant
mutmut show 42
JavaScript — Stryker¶
Java — PIT (pitest)¶
<!-- pom.xml -->
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.15.0</version>
</plugin>
Le coût du mutation testing
Le mutation testing est coûteux — il exécuté la suite de tests des dizaines ou centaines de fois. Reservez-le au code métier critique et executez-le en CI sur les changements (incremental), pas sur tout le projet à chaque push.
Intégrer dans le pipeline¶
graph LR
A["Push"] --> B["Tests"]
B --> C["Couverture"]
C -->|"< seuil"| D["Pipeline echoue"]
C -->|">= seuil"| E["Mutation testing<br/>(code modifie)"]
E --> F["Rapport"] Recommandation :
- Couverture : à chaque push, avec seuil bloquant
- Mutation testing : en nightly ou sur les MR touchant du code critique
Outils¶
| Outil | Type | Langage | Lien |
|---|---|---|---|
| coverage.py | Couverture | Python | coverage.readthedocs.io |
| c8 | Couverture | JavaScript | github.com/bcoe/c8 |
| Istanbul | Couverture | JavaScript | istanbul.js.org |
| JaCoCo | Couverture | Java | jacoco.org |
| mutmut | Mutation | Python | github.com/boxed/mutmut |
| Stryker | Mutation | JS/TS/.NET | stryker-mutator.io |
| PIT (pitest) | Mutation | Java | pitest.org |