Arc 4 Quête 17

Les Épreuves Automatiques

Tests automatisés, matrice, cache et artefacts

Tu avances dans les Forges Automatiques. Après avoir compris le fonctionnement des Pipelines (les chaînes d'assemblage enchantées), tu découvres une immense salle remplie de miroirs magiques et de balances de précision. Des parchemins sont suspendus dans les airs, tournant lentement tandis que des flammes colorées les inspectent sous tous les angles. Certains brillent d'un vert éclatant. D'autres rougissent et retombent dans un bac de rejet.

« Bienvenue dans la Salle des Épreuves. Ici, chaque chronique est soumise à des tests automatiques avant d'être acceptée dans la Grande Archive. Les Épreuves vérifient la forme du texte, la justesse de chaque calcul, la cohérence entre les chapitres, et le fonctionnement complet de l'ensemble. Aucun document ne passe sans avoir survécu à toutes les épreuves. »

Pourquoi automatiser les tests ?

Imagine un monde sans tests automatiques : chaque modification du code doit être vérifiée manuellement par un humain. C'est lent, sujet aux erreurs, et ça ne passe pas à l'échelle.

Les tests automatiques apportent plusieurs avantages cruciaux :

  • Détection précoce : les erreurs sont attrapées avant qu'un humain ne relise le code
  • Confiance : tu peux modifier du code existant sans craindre de tout casser
  • Rapidité : des centaines de vérifications en quelques secondes
  • Cohérence : les mêmes tests passent à chaque fois, sans oubli humain
  • Documentation vivante : les tests montrent comment le code est censé fonctionner

« Un Archiviste qui vérifie à la main chaque parchemin finit par s'épuiser et par laisser passer des erreurs. Les Épreuves Automatiques ne se fatiguent jamais. Elles sont les gardiennes infaillibles de la qualité. »

Les types d'épreuves automatiques

Il existe plusieurs niveaux de tests, du plus rapide au plus complet. Chacun a un rôle précis.

Le Linting - L'épreuve de forme

Le linting vérifie que le code respecte les règles de style et de formatage. Il ne teste pas si le code fonctionne - il vérifie qu'il est bien écrit.

Exemples de vérifications :

  • Le script commence-t-il par un shebang (#!/usr/bin/env bash) ?
  • Les indentations sont-elles cohérentes ?
  • Y a-t-il des espaces manquants ou en trop ?
  • Les variables sont-elles correctement nommées ?

Le linting est ultra-rapide et attrape les erreurs les plus simples. On le lance toujours en premier.

Les tests unitaires - L'épreuve de justesse

Les tests unitaires vérifient que chaque fonction individuelle produit le bon résultat. On isole une petite partie du code et on teste ses entrées/sorties.

Exemple : si tu as une fonction qui additionne deux nombres, un test unitaire vérifie que additionner 2 3 retourne bien 5.

# Exemple de test unitaire simple
resultat=$(./calculer.sh addition 2 3)
if [ "$resultat" = "5" ]; then
    echo "PASS: addition 2 + 3 = 5"
else
    echo "FAIL: attendu 5, obtenu $resultat"
fi

Les tests d'intégration - L'épreuve de cohérence

Les tests d'intégration vérifient que plusieurs composants fonctionnent ensemble. Un test unitaire vérifie une fonction en isolation. Un test d'intégration vérifie que les fonctions collaborent correctement.

Exemple : vérifier que la fonction de calcul et la fonction d'affichage produisent le bon résultat quand on les combine.

Les tests end-to-end - L'épreuve complète

Les tests end-to-end (bout en bout) simulent le parcours complet d'un utilisateur. Ils testent l'application entière, de l'entrée à la sortie.

Exemple : exécuter le script complet avec des arguments, vérifier que le fichier de sortie est correct, que les messages d'erreur sont appropriés, etc.

Les épreuves suivent un ordre naturel : d'abord la forme (linting), puis la justesse de chaque pièce (unitaire), puis la cohérence entre les pièces (intégration), et enfin le fonctionnement complet (end-to-end). Chaque niveau attrape des erreurs que le précédent ne voit pas.

La matrice de tests - Tester partout à la fois

Ton code doit fonctionner sur différents systèmes d'exploitation et différentes versions d'outils. La matrice de tests permet de lancer les mêmes tests sur toutes les combinaisons en parallèle.

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    node-version: [18, 20, 22]

Avec cette configuration, GitHub Actions crée 9 jobs (3 OS x 3 versions). Chaque combinaison est testée indépendamment.

On utilise les valeurs de la matrice dans le workflow :

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm test

« Les Épreuves de la Salle des Miroirs testent chaque chronique sous tous les angles possibles. Un parchemin qui fonctionne dans un miroir mais pas dans un autre est un parchemin défaillant. La matrice garantit une couverture totale. »

Le cache - Accélérer les épreuves

Télécharger les dépendances à chaque exécution du workflow est lent et gaspille des ressources. L'action actions/cache permet de sauvegarder ces fichiers entre les exécutions.

steps:
  - uses: actions/checkout@v4

  - name: Cache des dépendances
    uses: actions/cache@v4
    with:
      path: ~/.npm
      key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
      restore-keys: |
        ${{ runner.os }}-npm-

  - run: npm install
  - run: npm test

Le fonctionnement est simple :

  • path : le dossier à mettre en cache
  • key : un identifiant unique basé sur le système et le fichier de dépendances
  • restore-keys : des clés de secours si la clé exacte n'est pas trouvée

Si le fichier package-lock.json n'a pas changé, les dépendances sont restaurées depuis le cache au lieu d'être retéléchargées. C'est beaucoup plus rapide.

Les artefacts - Conserver les résultats

Les artefacts sont des fichiers produits pendant le workflow que tu veux sauvegarder après l'exécution. Les cas d'usage les plus courants : rapports de tests, fichiers de couverture, logs.

steps:
  - run: npm test -- --coverage

  - name: Sauvegarder le rapport de couverture
    uses: actions/upload-artifact@v4
    with:
      name: rapport-couverture
      path: coverage/
      retention-days: 30

Après l'exécution du workflow, tu retrouves les artefacts dans l'onglet "Actions" de GitHub, téléchargeables directement.

Pour récupérer un artefact dans un job ultérieur :

  - uses: actions/download-artifact@v4
    with:
      name: rapport-couverture

« Les résultats des Épreuves ne disparaissent pas dans le néant. Ils sont archivés - les rapports, les preuves, les mesures. Un bon Archiviste garde toujours une trace des vérifications effectuées. »

Les status checks obligatoires - Protéger la branche principale

Le véritable pouvoir des tests automatiques, c'est de les rendre obligatoires. Avec les required status checks, tu peux empêcher tout merge dans main si les tests ne passent pas.

Configuration sur GitHub :

  1. Va dans Settings > Branches
  2. Ajoute une Branch protection rule pour main
  3. Coche Require status checks to pass before merging
  4. Sélectionne les checks requis (le nom de tes jobs de workflow)
# Exemple : le job "tests" doit passer avant tout merge
name: CI
on:
  pull_request:
    branches: [main]

jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test

Avec cette configuration, si quelqu'un ouvre une Pull Request vers main et que les tests échouent, le bouton "Merge" sera bloqué. Personne ne peut fusionner du code cassé.

La Grande Archive est protégée par un sceau de qualité. Aucun parchemin ne peut y entrer sans avoir passé toutes les Épreuves. C'est la règle la plus importante des Forges : ce qui échoue aux tests ne merge pas.

La couverture de tests

La couverture de tests (test coverage) mesure le pourcentage de ton code qui est effectivement testé. C'est un indicateur utile mais imparfait.

  • 100% de couverture ne signifie pas que ton code est parfait - ça signifie que chaque ligne est exécutée au moins une fois
  • Faible couverture est un signal d'alarme - des parties entières de ton code ne sont jamais testées
  • Un bon objectif : viser 70-80% pour un projet standard

Les outils de couverture génèrent des rapports montrant quelles lignes sont testées (en vert) et lesquelles ne le sont pas (en rouge).

Patterns courants dans les workflows de test

Lancer le linter avant les tests

Le linting est rapide. Si le code est mal formaté, inutile de lancer des tests coûteux. On met le linter en premier :

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

  tests:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test

Le mot-clé needs crée une dépendance : le job tests ne démarre que si lint a réussi.

Fail fast vs run all

Par défaut, dans une matrice, si un job échoue, GitHub annule les autres. C'est le mode fail-fast :

strategy:
  fail-fast: true  # défaut : arrête tout dès qu'un job échoue
  matrix:
    os: [ubuntu-latest, windows-latest]

Si tu veux voir tous les résultats même quand certains échouent :

strategy:
  fail-fast: false  # continue même si un job échoue
  matrix:
    os: [ubuntu-latest, windows-latest]

Tester uniquement sur les Pull Requests

Pour économiser des ressources, tu peux limiter les tests aux Pull Requests :

on:
  pull_request:
    branches: [main]

Cela évite de relancer les tests à chaque push sur chaque branche. Les tests ne se déclenchent que quand quelqu'un propose de fusionner vers main.

« Un Maître des Épreuves sait organiser ses tests intelligemment. Les vérifications rapides passent en premier, les lentes en dernier. Les épreuves coûteuses ne sont lancées que lorsque c'est nécessaire. La sagesse, c'est aussi l'efficacité. »

Exercice pratique - Créer une suite d'épreuves automatiques

Crée tes propres Épreuves Automatiques :

  1. Crée un dépôt epreuves-automatiques
  2. Crée un script calculer.sh (calculatrice en ligne de commande)
  3. Crée un script de test test-calculer.sh
  4. Crée un workflow GitHub Actions avec un job de lint et un job de tests
  5. Utilise une matrice pour tester sur 2 systèmes d'exploitation
  6. Commite le tout et lance la vérification

Étape 1 - Créer le dépôt

mkdir epreuves-automatiques
cd epreuves-automatiques
git init -b main

Étape 2 - Créer le script de calcul

Crée un fichier calculer.sh qui effectue des opérations mathématiques simples :

cat > calculer.sh << 'EOF'
#!/usr/bin/env bash
# calculer.sh - Calculatrice simple en ligne de commande
# Usage : ./calculer.sh <operation> <nombre1> <nombre2>

operation="$1"
a="$2"
b="$3"

if [ -z "$operation" ] || [ -z "$a" ] || [ -z "$b" ]; then
    echo "Usage : ./calculer.sh <operation> <nombre1> <nombre2>"
    echo "Opérations : addition, soustraction, multiplication"
    exit 1
fi

case "$operation" in
    addition)
        echo $(( a + b ))
        ;;
    soustraction)
        echo $(( a - b ))
        ;;
    multiplication)
        echo $(( a * b ))
        ;;
    *)
        echo "Erreur : opération inconnue '$operation'"
        exit 1
        ;;
esac
EOF

Rends-le exécutable :

chmod +x calculer.sh

Teste-le manuellement :

./calculer.sh addition 2 3
# Doit afficher : 5

./calculer.sh multiplication 4 7
# Doit afficher : 28

Étape 3 - Créer le script de test

Crée un fichier test-calculer.sh qui vérifie automatiquement chaque opération :

cat > test-calculer.sh << 'EOF'
#!/usr/bin/env bash
# test-calculer.sh - Tests automatiques pour calculer.sh

SCRIPT="./calculer.sh"
ERREURS=0
TOTAL=0

tester() {
    local description="$1"
    local attendu="$2"
    local obtenu="$3"
    TOTAL=$((TOTAL + 1))

    if [ "$attendu" = "$obtenu" ]; then
        echo "  PASS: $description"
    else
        echo "  FAIL: $description (attendu: '$attendu', obtenu: '$obtenu')"
        ERREURS=$((ERREURS + 1))
    fi
}

echo "=== Tests de calculer.sh ==="
echo ""

echo "-- Tests d'addition --"
tester "2 + 3 = 5"       "5"  "$($SCRIPT addition 2 3)"
tester "0 + 0 = 0"       "0"  "$($SCRIPT addition 0 0)"
tester "100 + 200 = 300"  "300" "$($SCRIPT addition 100 200)"

echo ""
echo "-- Tests de soustraction --"
tester "10 - 3 = 7"      "7"  "$($SCRIPT soustraction 10 3)"
tester "5 - 5 = 0"       "0"  "$($SCRIPT soustraction 5 5)"

echo ""
echo "-- Tests de multiplication --"
tester "4 x 7 = 28"      "28" "$($SCRIPT multiplication 4 7)"
tester "0 x 100 = 0"     "0"  "$($SCRIPT multiplication 0 100)"
tester "3 x 3 = 9"       "9"  "$($SCRIPT multiplication 3 3)"

echo ""
echo "=== Résultat : $((TOTAL - ERREURS))/$TOTAL tests passés ==="

if [ "$ERREURS" -gt 0 ]; then
    exit 1
fi

exit 0
EOF

Rends-le exécutable et teste-le :

chmod +x test-calculer.sh
./test-calculer.sh

Tu devrais voir tous les tests passer.

Étape 4 - Créer le workflow GitHub Actions

Crée le dossier et le fichier de workflow :

mkdir -p .github/workflows
# .github/workflows/epreuves.yml
name: Épreuves Automatiques

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    name: Vérification de forme (lint)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Vérifier le shebang de calculer.sh
        run: |
          premiere_ligne=$(head -n 1 calculer.sh)
          if [ "$premiere_ligne" != "#!/usr/bin/env bash" ]; then
            echo "ERREUR : calculer.sh doit commencer par #!/usr/bin/env bash"
            exit 1
          fi
          echo "OK : shebang correct"

      - name: Vérifier le shebang de test-calculer.sh
        run: |
          premiere_ligne=$(head -n 1 test-calculer.sh)
          if [ "$premiere_ligne" != "#!/usr/bin/env bash" ]; then
            echo "ERREUR : test-calculer.sh doit commencer par #!/usr/bin/env bash"
            exit 1
          fi
          echo "OK : shebang correct"

  tests:
    name: Tests (${{ matrix.os }})
    needs: lint
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4

      - name: Rendre les scripts exécutables
        run: |
          chmod +x calculer.sh
          chmod +x test-calculer.sh

      - name: Lancer les tests
        run: ./test-calculer.sh

Pour écrire le fichier YAML, utilise la commande cat > .github/workflows/epreuves.yml << 'EOF' suivie du contenu, puis EOF sur une ligne seule.

Étape 5 - Commiter le tout

git add calculer.sh test-calculer.sh .github/workflows/epreuves.yml
git commit -m "Ajouter la calculatrice, les tests et le workflow CI"

Étape 6 - Vérifier la structure

# Le script de calcul existe et fonctionne
./calculer.sh addition 10 20

# Les tests passent
./test-calculer.sh

# Le workflow existe
cat .github/workflows/epreuves.yml

Étape 7 - Lancer la vérification

bash verifier.sh
.\verifier.ps1

Récapitulatif des concepts

Concept Description
Linting Vérification du style et du formatage du code
Tests unitaires Tests de fonctions individuelles en isolation
Tests d'intégration Tests de plusieurs composants ensemble
Tests end-to-end Tests du parcours complet de l'application
Matrice de tests Exécution parallèle sur plusieurs OS/versions
Cache Sauvegarde des dépendances entre les exécutions
Artefacts Fichiers produits pendant le workflow à conserver
Status checks Vérifications obligatoires avant un merge
Couverture de tests Pourcentage du code effectivement testé
Fail fast Arrêter tous les jobs dès qu'un échoue

Récapitulatif YAML

Élément Rôle
strategy: matrix: Définir les combinaisons OS/versions à tester
needs: job_name Créer une dépendance entre les jobs
fail-fast: false Continuer même si un job échoue
actions/cache@v4 Mettre en cache les dépendances
actions/upload-artifact@v4 Sauvegarder des fichiers produits
actions/download-artifact@v4 Récupérer des artefacts sauvegardés
on: pull_request: Déclencher uniquement sur les PR

Le Maître des Forges observe les miroirs de test tourner autour de ta chronique. Un par un, ils s'illuminent en vert. Addition - vert. Soustraction - vert. Multiplication - vert. Ubuntu - vert. macOS - vert. Le linting valide la forme. Les tests confirment la justesse.

« Tu as compris l'essence des Épreuves Automatiques. Chaque chronique que tu soumettras sera désormais passée au crible par des gardiens infatigables. Le linter vérifie la forme. Les tests unitaires vérifient la justesse. La matrice vérifie la compatibilité. Et les status checks interdisent l'entrée aux parchemins défaillants. »

Il désigne une section plus profonde des Forges, où des machines encore plus complexes assemblent des pièces entre elles automatiquement.

« Tu sais tester. Bientôt, tu sauras construire et déployer. Mais ça, c'est l'épreuve suivante. »