#Tests automatisés#Tests unitaires#Non-régression#Qualité logicielle#CI/CD

Les tests automatisés : du test unitaire à la non-régression

A

Alexandre Jeffroy

Ingénieur logiciel

||4 min de lecture

Un projet que je n'oublierai pas : un simulateur aéronautique de plusieurs centaines de milliers de lignes de C++, une équipe entière de développeurs. Une modification mineure sur un module de navigation, un vendredi. Le lundi, plusieurs scénarios de vol en approche étaient cassés. Plusieurs jours de chasse au bug, et du retard sur la livraison.

En cause : aucun test automatisé pour détecter la régression. On s'en est aperçu en recette manuelle, bien trop tard. Depuis, sur tous mes projets, les tests automatisés ne sont pas optionnels. Ils sont le filet de sécurité qui permet de modifier le code en confiance.

Pourquoi automatiser les tests

La première raison est la détection immédiate : un test exécuté en CI révèle le problème en minutes, pas en semaines. Vient ensuite la confiance dans les modifications, car une couverture solide permet de refactoriser sans craindre de casser l'existant. Un bon test fait aussi office de documentation vivante, puisqu'il décrit comment le code doit se comporter et devient obsolète bien plus vite qu'un commentaire s'il n'est plus à jour. Enfin, il rend la régression impossible : une fois un bug corrigé, j'écris un test qui verrouille la correction, et le bug ne peut plus revenir sans que le test ne le signale.

La pyramide des tests

Tous les tests ne se valent pas. Plus on monte dans la pyramide, plus les tests sont coûteux à écrire, lents à exécuter et fragiles : l'essentiel de la couverture doit venir des **tests unitaires**, complétés par des **tests d'intégration**, et un nombre limité de **tests fonctionnels** de bout en bout.

Un test unitaire, en pratique

Un bon test unitaire est rapide, indépendant, reproductible et auto-validant. Exemple sur un calcul de distance de freinage ferroviaire :

cpp
1class BrakingCalculator {
2public:
3    // vitesse en km/h, décélération en m/s²
4    double computeBrakingDistance(double speedKmh, double decelMs2) const {
5        if (speedKmh < 0.0 || decelMs2 <= 0.0)
6            throw std::invalid_argument("Invalid parameters");
7        const double speedMs = speedKmh / 3.6;
8        return (speedMs * speedMs) / (2.0 * decelMs2);   // d = v² / (2a)
9    }
10};
11
12TEST(BrakingCalculatorTest, StandardBrakingAt100KmH) {
13    BrakingCalculator calc;
14    // Attendu : environ 772 mètres, tolérance 1 mètre
15    EXPECT_NEAR(772.0, calc.computeBrakingDistance(100.0, 0.5), 1.0);
16}
17
18TEST(BrakingCalculatorTest, NegativeSpeedThrows) {
19    BrakingCalculator calc;
20    EXPECT_THROW(calc.computeBrakingDistance(-50.0, 1.0), std::invalid_argument);
21}

Pour isoler l'unité testée, on remplace les dépendances réelles (un capteur, un actionneur) par des mocks. C'est particulièrement utile pour tester un comportement de sécurité sans matériel réel :

cpp
1TEST(SafetyMonitorTest, UnhealthySensorTriggersDanger) {
2    MockPositionSensor sensor;
3    sensor.setPosition(500.0);  // position sûre
4    sensor.setHealthy(false);   // mais capteur défaillant
5
6    SafetyMonitor monitor(sensor, /*dangerZone=*/900.0);
7    EXPECT_TRUE(monitor.isInDangerZone());  // principe fail-safe
8}

Les tests d'intégration : vérifier les interactions

Les tests unitaires vérifient des unités isolées ; les tests d'intégration vérifient que ces unités fonctionnent correctement ensemble, par exemple un contrôleur, ses capteurs et ses actionneurs dans un scénario complet de freinage d'urgence. Ils sont plus lents, mais indispensables pour valider les interactions réelles.

Sur les systèmes critiques : couverture et traçabilité

Les normes de certification (DO-178C en aéronautique, CENELEC EN 50128 en ferroviaire) imposent des taux de couverture stricts selon la criticité, mesurés avec des outils comme gcov/lcov. Chaque anomalie détectée doit donner lieu à un test qui la reproduit, intégré durablement à la suite de non-régression. Et chaque exigence doit être couverte par au moins un test tracé :

cpp
1// REQ-042 : le système doit activer le freinage d'urgence
2//           dans les 200 ms suivant la détection du danger
3TEST(BrakingSystemTest, REQ_042_EmergencyBrakingResponse) {
4    auto start = std::chrono::high_resolution_clock::now();
5    system_.detectDanger();
6    auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
7        std::chrono::high_resolution_clock::now() - start).count();
8
9    EXPECT_TRUE(system_.isEmergencyBraking());
10    EXPECT_LE(elapsed, 200);
11}

Ce qu'il faut retenir

Les tests automatisés n'ont de valeur que s'ils s'exécutent systématiquement, à chaque commit, dans la CI. Écrire le test, l'intégrer, le garder vert : c'est cette discipline qui transforme une suite de tests en véritable garantie de non-régression, et qui permet de faire évoluer un système critique sans craindre de casser ce qui fonctionnait déjà.