Lecodeestdanslepre

Les tests, ou bien comment jouer au Cluedo

16 min de lecture

« Une dernière chose... » murmure l'inspecteur Colombo en se retournant lentement, son imperméable froissé traînant sur le carrelage immaculé du département IT. « Ce bug dans la production... comment expliquez-vous qu'il soit passé inaperçu ? »

Le crime parfait n'existe pas (même en PHP)

Dans le manoir du développement web, un crime a été commis. Le Colonel Moutarde (alias votre code de production) a été retrouvé inerte sur la production provoquant une belle Exception. Mais qui est le coupable ? Avec quelle arme ? Et surtout, comment aurait-on pu prévenir ce drame ?

Bienvenue dans le monde impitoyable des tests unitaires où chaque ligne de code est un suspect potentiel, et où vous, cher développeur, êtes à la fois l'assassin et le détective.

La scène du crime : le code au sein de ton application Symfony

Notre luxueux manoir victorien, à savoir votre code en production, offre un cadre élégant pour commettre vos méfaits de code. Avec ses nombreuses pièces (bundles), ses passages secrets (événements) et son personnel de maison dévoué (services), c'est l'endroit idéal pour dissimuler un bug.

Mais attention car chaque crime laisse des traces, et c'est là qu'intervient notre fidèle inspecteur : PHPUnit.

L'inspecteur PHPUnit : « Je sais que c'est vous qui avez codé ça... »

Comme Colombo avec son carnet défraîchi et ses questions innocentes, PHPUnit ne paie pas de mine mais ne laisse rien passer. Son imperméable froissé cache une méthodologie implacable :

// La méthode d'investigation classique de l'inspecteur
public function testVotreAlibiNeTientPas(): void
{
    $service = new ServiceSuspect();
    $resultat = $service->methodeDouteuse(42);

    $this->assertEquals('résultat attendu', $resultat, 'Votre code ment, j\'en mettrais ma main au feu !');
}

Les armes du crime dans PHP 8.4

PHP 8.4 a introduit un arsenal redoutable pour les développeurs-assassins tels que :

  1. Le chandelier des types union plus précis - Une arme élégante qui frappe avec des types plus stricts
  2. Le poignard des fonctions anonymes améliorées - Discret, mortel, impossible à tracer
  3. Le revolver des attributs étendus - Six coups pour décorer votre code avant qu'il ne s'effondre

Mais attention, car chaque arme laisse une signature unique que notre détective saura identifier.

La stratégie du Cluedo : tester chaque combinaison

Dans le jeu du Cluedo, on procède par élimination : « Est-ce Mme Leblanc dans la cuisine avec le chandelier ? »

Nos tests unitaires fonctionnent exactement de la même façon avec Symfony :

// La méthode du Cluedo appliquée aux tests
#[DataProvider('fournirSuspects')]
public function testChaqueScenarioPossible(string $suspect, string $lieu, string $arme): void
{
    $detective = self::getContainer()->get(DetectiveService::class);
    $verdict = $detective->resoudre($suspect, $lieu, $arme);

    if ($suspect === 'DéveloppeurDistrait' && $lieu === 'ControleurPrincipal' && $arme === 'CopierColler') {
        $this->assertTrue($verdict->estCoupable());
    } else {
        $this->assertFalse($verdict->estCoupable());
    }
}

« Je vous dérange, mais j'ai encore une question... »

L'art des tests unitaires, comme celui de Colombo, repose sur l'art de poser LA question qui dérange. Cette question qui fait s'effondrer tout l'alibi :

Les questions qui dérangent :

  1. « Et si ce paramètre était null ? » - Le classique qui fait transpirer
  2. « Pourquoi votre fonction accepte un float mais retourne un int ? » - L'incohérence révélatrice
  3. « Comment expliquez-vous ce comportement quand la base de données est indisponible ? » - Le piège fatal

Le développeur face à son crime

Dans cette partie de Cluedo développemental, rappelez-vous que :

  1. Vous êtes à la fois le criminel (qui écrit le code bogué) et le détective (qui écrit les tests)
  2. Chaque fonction non testée est une pièce du manoir où le Colonel Moutarde pourrait être assassiné
  3. Le but n'est pas d'arrêter le coupable, mais d'empêcher le crime d'avoir lieu

La prochaine fois que vous vous apprêtez à commit du code sans tests, imaginez l'inspecteur PHPUnit s'approchant lentement de vous, mâchouillant son cigare éteint, et murmurant : « Une dernière chose... Comment comptez-vous dormir tranquille sans tests unitaires ? »

Car au Cluedo du développement, on sait déjà que c'est vous qui avez tué la qualité du code dans la bibliothèque avec le code mal testé. Tout ce qu'il nous reste à faire, c'est de prouver comment et pourquoi.

Alors, qu'attendez-vous pour confesser... vos tests ?

TDD : quand l'enquête précède le crime

« Vous savez, madame, » dit Colombo en allumant son cigare malgré les panneaux d'interdiction, « les meilleurs détectives ne cherchent pas à résoudre les crimes... ils les empêchent avant qu'ils ne se produisent. »

Le principe de précognition policière

Imaginez un univers parallèle où l'inspecteur Colombo arriverait sur les lieux AVANT que le meurtre ne soit commis. Il installerait des caméras, noterait ses attentes concernant les comportements suspects, et déclencherait l'alarme au moindre écart au scénario prévu.

C'est exactement ça, le Test-Driven Development (TDD).

Les trois actes du TDD : le plan parfait

Acte 1 : préparer l'arrestation

Dans la salle d'interrogatoire, vous commencez par écrire le procès-verbal de l'arrestation... avant même que le crime ne soit commis :

public function testTransfertBancaireReussit(): void
{
    $compte1 = new Compte(solde: 1000);
    $compte2 = new Compte(solde: 0);

    $service = new ServiceTransfert();
    $resultat = $service->transferer(500, $compte1, $compte2);

    $this->assertTrue($resultat->estReussi());
    $this->assertEquals(500, $compte1->getSolde());
    $this->assertEquals(500, $compte2->getSolde());
}

Ce test échoue. Évidemment, puisque le criminel (votre code) n'a pas encore agi.

Acte 2 : provoquer le crime contrôlé

Maintenant, vous écrivez le minimum de code nécessaire pour satisfaire les conditions de l'arrestation :

class ServiceTransfert 
{
    public function transferer(float $montant, Compte $source, Compte $destination): Resultat
    {
        $source->debiter($montant);
        $destination->crediter($montant);

        return new Resultat(true);
    }
}

Comme par magie, votre code "criminel" se conforme exactement au scénario que vous aviez prévu.

Acte 3 : nettoyer la scène de crime

« Une scène de crime parfaite ne laisse aucune trace, » marmonne Colombo en réarrangeant méticuleusement les objets.

class ServiceTransfert 
{
    public function transferer(float $montant, Compte $source, Compte $destination): Resultat
    {
        if ($source->getSolde() < $montant) {
            return new Resultat(false, 'Fonds insuffisants');
        }

        $source->debiter($montant);
        $destination->crediter($montant);

        return new Resultat(true);
    }
}

Le carnet du détective : les avantages du TDD

Dans la marge de son carnet usé, l'inspecteur a noté :

  1. Prévention plutôt que Guérison - « Plus facile d'empêcher un meurtre que de résoudre une enquête »
  2. Documentation Vivante - « Chaque test raconte l'histoire du crime qui n'a pas eu lieu »
  3. Confiance en Soi - « Je peux dormir tranquille sachant que les caméras de surveillance fonctionnent »

La technique du Cluedo inversé

Dans ce "Cluedo inversé" du TDD, nous commençons par la solution : « C'est le professeur Plum, dans la bibliothèque, avec le chandelier. » Puis nous construisons l'histoire qui mène à cette conclusion.

Ce renversement de perspective est précisément ce qui fait du TDD une méthodologie si puissante.

L'Art de l'enquête préventive

Que vous soyez adepte de Colombo traquant les indices après le crime ou partisan du TDD pour anticiper les problèmes, l'essentiel est de comprendre que dans le développement comme dans le polar, la vérité finit toujours par éclater.

Et comme dirait notre inspecteur préféré en quittant la scène : « Ah, juste une dernière chose... n'oubliez pas d'exécuter vos tests avant chaque commit. On ne sait jamais qui pourrait être le prochain Colonel Moutarde. »

Les témoins sous protection : l'art du mock et des doublures

« Parfois, madame, » murmure Colombo en s'approchant confidentiellement, « pour coincer un criminel, il faut remplacer les vrais témoins par des acteurs qui suivent notre script. »

Le programme de protection des témoins

Dans notre univers de tests unitaires, nous ne pouvons pas toujours compter sur de vrais services externes. La base de données pourrait mentir, l'API pourrait disparaître mystérieusement, et le serveur SMTP pourrait être en vacances aux Bahamas.

C'est là qu'intervient notre "Programme de protection des témoins" : les mocks et les doublures de test.

Les différents types d'informateurs

// Le témoin amnésique (Dummy)
$dummyLogger = $this->createMock(LoggerInterface::class);
// Ne fait rien, existe juste pour satisfaire les dépendances

// L'indic qui raconte toujours la même histoire (Stub)
$stubRepository = $this->createMock(UtilisateurRepository::class);
$stubRepository->method('findOneBy')
               ->willReturn(new Utilisateur('Columbo'));

// Le témoin sous surveillance (Spy)
$spyMailer = $this->createMock(MailerInterface::class);
$spyMailer->expects($this->once())
          ->method('send');

// L'acteur qui joue le rôle parfaitement (Mock)
$mockValidator = $this->createMock(ValidatorInterface::class);
$mockValidator->expects($this->exactly(2))
             ->method('validate')
             ->withConsecutive(
                 [$this->isInstanceOf(Utilisateur::class)],
                 [$this->isInstanceOf(Adresse::class)]
             )
             ->willReturnOnConsecutiveCalls(
                 new ConstraintViolationList(),
                 new ConstraintViolationList([new ConstraintViolation(/* ... */)])
             );

L'interrogatoire parfait avec Symfony

Symfony offre des outils d'interrogatoire sophistiqués dans sa salle d'enquête KernelTestCase et WebTestCase :

class EnqueteControllerTest extends WebTestCase
{
    public function testSceneDeCrime(): void
    {
        // Préparation de la reconstitution
        $client = static::createClient();

        // Mise en scène du crime
        $client->request('GET', '/scene-de-crime/42');

        // Vérification des preuves
        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Indice trouvé');
    }
}

La surveillance continue : CI/CD, le détective qui ne dort jamais

« Vous savez ce qui fait un bon détective ? » demande Colombo en regardant par la fenêtre. « Ce n'est pas l'intelligence, ni même l'expérience. C'est la persévérance. Ne jamais abandonner. Surveiller sans relâche. »

Le système de surveillance automatisé

Imaginez Colombo qui, au lieu de mener lui-même toutes ses enquêtes, aurait mis en place un réseau de caméras de surveillance, d'alarmes et d'agents automatisés qui vérifient en permanence que tout est en ordre.

C'est exactement ce qu'est l'Intégration Continue pour votre code Symfony.

Le rapport de surveillance GitHub Actions

# .github/workflows/tests.yaml
name: Tests de l'Alibi

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

jobs:
  enquete:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Configuration de PHP 8.4
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.4'

    - name: Vérification des Preuves
      run: |
        composer install
        php bin/phpunit

    - name: Rapport d'Autopsie
      if: ${{ failure() }}
      run: cat var/log/test.log

Les rapports d'activité suspects : PHPUnit avec Code Coverage

Comme tout bon détective, nous voulons savoir exactement quelles parties du code (quelles pièces du manoir) ont été inspectées :

# Lancer l'enquête complète avec rapport de couverture
$ php bin/phpunit --coverage-html var/coverage

Le rapport résultant vous montre une carte du manoir avec les pièces visitées en vert et les zones d'ombre en rouge – précisément là où pourrait se cacher le Colonel Moutarde.

L'art de la déduction PHP

Nous voici à la fin de notre enquête sur les tests dans Symfony. Comme l'aurait dit Sherlock Holmes (cousin éloigné de Colombo dans l'univers des détectives) :

« Quand vous avez éliminé l'impossible, ce qui reste, aussi improbable soit-il, doit être la vérité. »

Dans notre cas :

  1. L'impossible : Un code sans bug
  2. L'improbable : Un code avec des tests complets
  3. La vérité : Avec des tests bien faits, vous dormez mieux la nuit

Alors, cher développeur-détective, la prochaine fois que vous vous retrouverez face à un meurtre de production, rappelez-vous : les meilleurs crimes sont ceux qui n'ont jamais lieu, et les meilleurs tests sont ceux qui sont écrits avant même que le code criminel n'existe.

Et si jamais vous croisez un imperméable froissé dans les couloirs de votre open-space, méfiez-vous : il se pourrait bien que ce soit PHPUnit qui vienne vous demander : « Juste une dernière chose... »

Les reconstitutions de crime : Behat et les tests comportementaux

« Pour comprendre un meurtrier, » philosophe Colombo en remontant le col de son imperméable, « il faut reconstituer la scène exactement comme elle s'est produite. Pas seulement les faits, mais l'intention, le comportement. »

L'approche BDD : Behaviour-Driven Deduction

Nous avons notre propre méthode de reconstitution de crime : Behat et le BDD (Behavior-Driven Development).

Imaginez que nous écrivions le scénario d'un épisode de Colombo, mais pour notre application :

# features/transfert_argent.feature
Fonctionnalité: Transfert d'argent entre comptes
  En tant que client de la banque
  Je veux pouvoir transférer de l'argent entre mes comptes
  Afin de gérer mes finances

  Scénario: Transfert réussi entre deux comptes
    Étant donné que j'ai un compte "Courant" avec un solde de "1000" euros
    Et que j'ai un compte "Épargne" avec un solde de "500" euros
    Quand je transfère "300" euros du compte "Courant" vers le compte "Épargne"
    Alors le solde du compte "Courant" devrait être de "700" euros
    Et le solde du compte "Épargne" devrait être de "800" euros

Puis nous "tournons" cet épisode avec nos acteurs de code :

// features/bootstrap/FeatureContext.php
/**
 * @Given j'ai un compte :type avec un solde de :montant euros
 */
public function jaiUnCompteAvecUnSoldeDe($type, $montant)
{
    $this->comptes[$type] = new Compte();
    $this->comptes[$type]->deposer((float) $montant);
}

/**
 * @When je transfère :montant euros du compte :source vers le compte :destination
 */
public function jeTransfereEurosDeVers($montant, $source, $destination)
{
    $this->service->transferer(
        (float) $montant,
        $this->comptes[$source],
        $this->comptes[$destination]
    );
}

Le langage des témoins

Le génie de cette approche, c'est que même Mme Columbo (qui n'apparaît jamais à l'écran mais dont on parle souvent) pourrait comprendre ces scénarios. Le langage Gherkin est si proche du langage naturel que n'importe quel "civil" peut suivre l'enquête.

L'unité spéciale des crimes temporels : les tests de performance

« Le temps, lieutenant, » dit le suspect en costume trois pièces, « est le seul luxe que je ne peux me permettre de gaspiller. »

« Intéressant, » répond Colombo en consultant sa montre cabossée. « Parce que selon mon enquête, votre application a pris exactement 2.3 secondes pour répondre. C'est... comment dire... suspicieusement lent, non ? »

Le chronomètre de l'inspecteur

Dans notre arsenal Symfony, nous avons des outils spécialisés pour traquer les coupables qui volent le temps précieux de nos utilisateurs :

// tests/Performance/TransactionPerformanceTest.php
public function testPerformanceTransfert(): void
{
    $client = self::createClient();

    $startTime = microtime(true);

    // Exécution de 100 transferts
    for ($i = 0; $i < 100; $i++) {
        $client->request('POST', '/api/transfert', [
            'source' => 'compte1',
            'destination' => 'compte2',
            'montant' => 10
        ]);
    }

    $duration = microtime(true) - $startTime;

    // Le verdict de temps
    $this->assertLessThan(
        2.0, 
        $duration, 
        "L'opération a pris $duration secondes, ce qui dépasse notre seuil acceptable de 2 secondes"
    );
}

Les Cold Cases : les tests de régression

« Vous savez ce qui me tracasse dans cette affaire ? » demande Colombo en grattant sa tête ébouriffée. « C'est que j'ai l'impression d'avoir déjà vu ce mode opératoire quelque part... »

Les bugs de régression sont comme les tueurs en série qui reviennent sur les lieux du crime : ils suivent un pattern reconnaissable, et souvent ils frappent deux fois au même endroit.

// tests/Regression/BugFix1234Test.php
public function testRegressionBug1234(): void
{
    // 1. Reproduire exactement les conditions du crime passé
    $service = self::getContainer()->get(ProduitService::class);
    $produit = new Produit();
    $produit->setPrix(0.0);

    // 2. Vérifier que notre coupable ne peut plus opérer de la même façon
    $resultat = $service->calculerTaxe($produit);

    // Le bug était une division par zéro, vérifions qu'il est bien résolu
    $this->assertEquals(0.0, $resultat);
}

Chaque test de régression est comme un dossier d'archives policières : il témoigne d'une affaire résolue et nous protège contre un criminel récidiviste.

L'académie de police : former les nouveaux détectives

« Lieutenant, » soupire Colombo face à un jeune policier débutant, « un bon détective ne naît pas, il se forme. Chaque enquête est une leçon. »

Dans le monde du développement, les tests sont aussi un excellent outil pédagogique :

  1. Les Tests comme Documentation - Ils racontent l'histoire fonctionnelle de votre application
  2. La Revue de Code par les Tests - « Montre-moi tes tests, je te dirai qui tu es »
  3. L'Apprentissage par le TDD - Rien de tel que d'écrire le test avant pour comprendre vraiment ce qu'on veut construire
// Un test éloquent qui enseigne plus qu'un livre
public function testProcessusCompletInscription(): void
{
    // 1. Préparation du suspect
    $email = 'nouveau@exemple.com';
    $motDePasse = 'Tr3sS3cur1s3!';

    // 2. Exécution de l'action
    $utilisateur = $this->service->inscrire($email, $motDePasse);

    // 3. Vérifications
    $this->assertNotNull($utilisateur->getId(), "L'utilisateur doit être persisté");
    $this->assertTrue(
        $this->hasher->verify($utilisateur->getMotDePasse(), $motDePasse),
        "Le mot de passe doit être hashé correctement"
    );
    $this->assertEquals(1, count($this->getMailsSent()), "Un email doit être envoyé");
}

Ce test raconte toute une histoire : il parle d'inscription, de sécurité, de persistance et de communication. C'est un manuel de formation pour les nouveaux venus dans le projet.

Et maintenant, il est temps de conclure véritablement notre enquête...

Créer une culture de la preuve

« Voyez-vous, » conclut Colombo en rangeant définitivement son carnet, « résoudre un crime, c'est bien. Mais créer une société où le crime ne paie pas, c'est mieux. »

Cela signifie cultiver une véritable culture du test :

Les règles du département

  1. La règle des 80% - Aucun code ne franchit la ligne jaune sans au moins 80% de couverture de test
  2. La politique de la preuve - Chaque nouvelle fonctionnalité doit apporter ses propres preuves (tests)
  3. L'alibi permanent - Les tests doivent passer à tout moment, ou l'intégration continue bloque le déploiement
  4. La formation continue - Chaque membre de l'équipe doit pouvoir écrire et comprendre les tests
  5. Le code de conduite - La qualité des tests est aussi importante que la qualité du code testé

Les bénéfices d'une justice bien rendue

Une telle culture apporte ses récompenses :

  1. Moins de récidive - Les bugs connus ne reviennent pas
  2. Confiance publique - Les utilisateurs font confiance à un système fiable
  3. Réactivité policière - Les nouveautés peuvent être déployées sans crainte
  4. Travail d'équipe - Les tests permettent de collaborer sans se marcher sur les pieds
  5. Sommeil tranquille - Plus de nuits blanches à debugger en production à 3h du matin

Mot de la fin

Comme tout bon épisode de Colombo, notre histoire se termine par une révélation :

« Vous savez ce qui est vraiment fascinant dans ce métier ? » demande l'inspecteur en s'éloignant, « ce n'est pas d'attraper les coupables... c'est de protéger les innocents. »

Dans notre monde du développement, les tests ne sont pas là pour punir les mauvais développeurs, mais pour protéger les bons développeurs des erreurs inévitables que nous commettons tous.

Alors la prochaine fois que vous vous apprêtez à écrire du code sans test, imaginez un petit homme en imperméable froissé qui se retourne et vous dit avec un sourire en coin :

« Ah, juste une dernière chose… vous avez pensé à écrire le test d'abord ? »

Confidentialité

Ce site utilise Umami pour analyser le trafic de manière anonyme. Acceptez-vous la collecte de données anonymes ?