Aller au contenu

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

  1. Le framework crée des mutants — des copies du code avec une petite modification (mutation)
  2. Les tests sont exécutés sur chaque mutant
  3. Si les tests échouent → le mutant est tue (bien)
  4. 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 truereturn false
Suppression d'instruction Supprimer une ligne de code
Negation de condition if (x)if (!x)
Valeur constante 01, """mutant"

Interpretation du score

Score de mutation = mutants tues / mutants totaux × 100
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

# Avec Vitest
vitest --coverage

# Avec Jest
jest --coverage

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

npm install --save-dev @stryker-mutator/core
npx stryker init

# Lancer
npx stryker run

Java — PIT (pitest)

<!-- pom.xml -->
<plugin>
    <groupId>org.pitest</groupId>
    <artifactId>pitest-maven</artifactId>
    <version>1.15.0</version>
</plugin>
mvn org.pitest:pitest-maven:mutationCoverage

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