Bibliothèque · Résumé et avis

Test-Driven Development with PHP 8

De Rainier Sarabia. Un fil complet du ticket à la CI avec Behat et SOLID piloté par les tests, sur un décor Symfony 6 daté.

FR EN
Couverture de Test-Driven Development with PHP 8, Rainier Sarabia

Test-Driven Development with PHP 8

Test-Driven Development with PHP 8: Build extensible, reliable, and maintainable enterprise-level applications using TDD and BDD with PHP

5.2 /10

« Un bon fil du besoin au test, un emballage Docker, PHPUnit 9 et Goutte qui a pris la poussière. »

  • AuteurRainier Sarabia
  • VOPackt, 2023 · version cours ExpertEdge, 5 h 36
  • LangueEn anglais uniquement
  • Fiche~7 min de lecture
  • Le lire Lire sur ExpertEdge Code : -50 % sur l'abonnement annuel. Lien affilié : je touche une commission si vous vous abonnez avec ce code. Mes notes n'en dépendent pas.
Notation du livre sur 5 dimensionsIdées5/10Applicable6/10Lisibilité6/10Actualité4/10Exemples5/10

Un fil complet du ticket à la CI avec Behat et SOLID piloté par les tests, sur un décor Symfony 6 daté.

Pourquoi ce livre

Ma bibliothèque a déjà Kent Beck, qui donne la méthode du TDD (Test-Driven Development: By Example), et Matt Zandstra, qui donne les objets PHP (PHP 8 Objects, Patterns, and Practice). Il manquait le chemin complet pour un dev Symfony, du besoin du client au pipeline. J'ai fait Symfony 2 et 3 chez Netime, Magento et Symfony chez Decitre, PHP 8 chez Teranga-Software. Ancien formateur DWWM et jury de titres, je cherchais un cours pour un reconverti.

Rainier Sarabia, responsable d'ingénierie logicielle installé en Australie, publie ce livre chez Packt en janvier 2023. Lu sur ExpertEdge : 5 h 36 de cours, onze sections. L'ouverture plaide pour le TDD, « un processus et une façon de penser » (leçon 3), et démonte quatre idées reçues (leçon 4). Le livre reste pourtant mince pour ce qu'il promet. La moitié du cours est du pas-à-pas daté (Docker, Jira, console AWS) ou du rappel de POO.

Le mot Symfony revient 253 fois dans le texte, surtout dans des chemins comme codebase/symfony/tests/Unit. Le fil rouge (un inventaire de petites voitures pour un musée) tourne sur un squelette Symfony 6 avec Doctrine (l'ORM, qui traduit les objets PHP en lignes de base de données) et MySQL 8. Le framework sert surtout de décor, sauf pour ses générateurs de code et un test d'intégration (idée 6).

Les idées qui restent

1Le besoin d'abord : le code vient après

Pas une ligne de PHP au début. Des bénévoles du musée consignent les dons dans un tableur partagé : la couleur d'une voiture s'y écrit « blue », « flames » ou avec une faute de frappe, et personne ne retrouve rien. Sarabia en tire des besoins, des problèmes et des contraintes, puis des user stories (la fonctionnalité décrite du point de vue de l'utilisateur) rangées dans des epics (les grands lots) sous Jira, l'outil de suivi de tâches. Écrire le code directement à partir d'une story est pour lui « une recette pour un futur désastre » (leçon 7), et il avoue : « c'est ainsi que je développais » (leçon 7). Sa chaîne : le besoin devient un scénario, le scénario un test, le test dicte le code.

2Gherkin, Behat, Mink : une langue commune pour le métier

Le BDD (Behavior-Driven Development, développement piloté par le comportement) fait écrire d'abord ce que le système doit faire, dans une langue que le métier relit. Gherkin est cette langue : « un langage pour définir des scénarios et des objectifs métier » (leçon 34), en anglais simple. Behat est le framework PHP qui exécute ces scénarios. Mink est une bibliothèque qui pilote un navigateur simulé, « un robot de vous-même qui teste votre programme dans un navigateur » (leçon 34).

Feature: Home page
  In order to welcome visitors
  As a visitor
  I need to be able to see the Symfony logo
  Scenario: See the Symfony logo
    Given I have access to the home page URL
    When I visit the home page
    Then I should see the Symfony Logo

Given pose l'état de départ, When l'action, Then le résultat attendu. Behat génère les méthodes PHP vides (--append-snippets), à remplir. Sarabia avait voulu sauter Gherkin, jusqu'à un projet avec une société tierce dont les tests vivaient dans son application : « J'aurais voulu un format commun que nous puissions partager » (leçon 36).

3Un fil unique du ticket au test unitaire

Beck, dans son livre, reste sur le test unitaire (le test d'une seule classe ou méthode, sans base ni réseau). Ici, les niveaux s'enchaînent sur un même ticket, l'inscription d'un préposé à l'inventaire du musée. Ce ticket devient une feature Gherkin que Behat exécute et qui échoue. Un test fonctionnel Symfony échoue ensuite : il envoie une requête HTTP à un contrôleur et vérifie la réponse. Vient un test d'intégration, qui fait travailler plusieurs classes avec une vraie base, dédiée aux tests, puis les tests unitaires. Pour passer au vert, l'auteur s'appuie sur le composant de sécurité : « Heureusement, Symfony rend la sécurité facile » (leçon 40). Les générateurs make:user et make:registration-form écrivent l'utilisateur et l'inscription (leçons 40 et 41).

Un ticket traverse six étapes : le ticket, la feature Gherkin exécutée par Behat, le test fonctionnel, le test d'intégration, les tests unitaires, puis la CI 1 Le ticket inscription d'un préposé à l'inventaire du musée 2 Feature Gherkin, exécutée par Behat elle échoue 3 Test fonctionnel Symfony requête HTTP vers un contrôleur, réponse vérifiée 4 Test d'intégration plusieurs classes avec la vraie base 5 Tests unitaires une classe ou une méthode, sans base ni réseau 6 La CI tout est rejoué à chaque push un seul ticket traverse tous les niveaux
Les étapes 2 à 5 rétrécissent le champ : de la page vue par l'utilisateur jusqu'à une classe isolée

4Rouge d'abord, même pour un test qui ne teste rien

La première règle est « il faut toujours faire échouer le test d'abord » (leçon 29), pour quatre motifs (leçon 3). Le premier : prouver que PHPUnit reconnaît le test, donc que l'environnement est bien configuré. D'où les cycles ouverts par $this->fail("--- RED ---"). Les trois autres : dire ce que le code doit faire, lister le travail restant, garantir que ce code sera testé. Le premier vrai exemple (leçon 24) échoue par une soustraction placée exprès :

// ✗ AVANT : le test attend 6, il reçoit 0
return $a + $b - $c;
// ✓ APRÈS : on additionne $c, le test passe
return $a + $b + $c;

Pour passer au vert, la règle est de faire « le minimum pour passer le test » (leçon 38).

Les tests se rangent en suites (Unit, Integration) déclarées dans phpunit.xml, la configuration de PHPUnit. L'auteur a connu une suite unique de milliers de tests : « on la lance et on attend une heure pour découvrir un seul test unitaire cassé » (leçon 28). Sur la couverture de code (la part du code exécutée par les tests), « 50 % valent mieux que 0 % » (leçon 25).

5SOLID piloté par des tests qui échouent

SOLID est un sigle de cinq principes de conception :

  • S (responsabilité unique) : une classe, une raison de changer.
  • O (ouvert/fermé) : ouvert à l'extension, fermé à la modification.
  • L (Liskov) : une implémentation doit pouvoir en remplacer une autre sans casser le comportement.
  • I (interfaces étroites) : pas de méthode inutile imposée aux classes.
  • D (inversion des dépendances) : dépendre d'abstractions plutôt que de classes concrètes.

Le fil rouge est un validateur de voiture, et chaque principe naît d'un test rouge. Pour le S, l'objet ToyCar ne contient que des données, une classe à part orchestre. Pour le O, un YearValidator et un NameValidator, chacun testé, se glissent dans un tableau, et un futur validateur de couleur n'obligera pas à rouvrir la classe principale :

$validator = new ToyCarValidator();
$validator->setValidators([
    'year' => new YearValidator(),
    'name' => new NameValidator(),
]);

Pour le L, le validateur d'année lève une ToyCarTooOldException qui étend \Exception. Personne ne l'attrape et le test plante, parce que l'interface promet une autre exception.

// ✗ AVANT : hors du contrat de l'interface
class ToyCarTooOldException extends \Exception {}
// ✓ APRÈS : l'interface déclare ToyCarValidationException
class ToyCarTooOldException extends ToyCarValidationException {}

Pour le I, ajouter validateLength à l'interface commune ne convient pas à un validateur de couleur : la couleur est un objet ToyColor, pas une chaîne. On sort donc une StringValidatorInterface. Sarabia avoue des interfaces pleines de méthodes vides et de « TODO ».

6Injecter, doubler, et garder le domaine loin de Doctrine

Pour le D, Sarabia juge ce principe « le plus important pour rendre une classe testable » (leçon 51). Le validateur ne crée plus ses dépendances, on les lui passe. Le test remplace les classes manquantes par des doublures (des faux objets dont on fixe la réponse), ce qui évite d'attendre le collègue qui met deux semaines à écrire l'enregistrement en base :

$validatorStub = $this->createMock(ToyCarValidatorInterface::class);
$validatorStub->method('validate')->willReturn($validationResultStub);
$toyWriterStub = $this->createMock(WriterInterface::class);
$toyWriterStub->method('write')->willReturn(true);
$processor = new ToyCarCreator($validatorStub, $toyWriterStub);

ToyCarCreator, la classe de traitement (le livre dit processeur), valide puis écrit. Elle manipule un objet ToyCar sans annotation Doctrine (un commentaire spécial qui relie une classe à une table), un POPO (objet PHP sans dépendance). Le livre donne pourtant le même nom à l'entité Doctrine (leçon 30), que ses annotations lient à l'ORM. Pour SOLID, Sarabia injecte à la main.

Symfony sait injecter seul, et le livre le montre une fois (leçon 31). Un service de café y enregistre un total dans la base de test. Le fichier services.yaml, où Symfony déclare ses services, active l'autowiring : Symfony lit le constructeur et fournit seul les objets demandés. Le test étend KernelTestCase, la classe de test qui démarre Symfony. Il demande le service au conteneur, l'objet qui fabrique les services, au lieu d'écrire new.

Le même ToyCarCreator reçoit soit le validateur et le writer réels, soit deux doublures Classes réelles Doublures du test ToyCarCreator ToyCarCreator reçoit reçoit reçoit reçoit validateur réel writer réel, en base doublure du validateur doublure du writer même classe, seul le contenu des deux boîtes change
À droite, le test n'attend ni la base ni le collègue qui l'écrit

7La CI rejoue tout à chaque push

La CI (intégration continue) fait tourner les tests à chaque modification. Bitbucket Pipelines, le service de CI de Bitbucket, lit un fichier YAML. Celui du livre (extrait, installation de l'image omise) monte les conteneurs Docker. Il joue ensuite les tests Symfony avec couverture et les scénarios Behat (cinq annoncés, quatre écrits) :

script:
  - cd docker
  - docker-compose build && docker-compose up -d
  - docker exec -i docker_server-web_1 /var/www/html/symfony/setup.sh
  - docker exec -i docker_server-web_1 /var/www/html/symfony/runCoverage.sh
  - docker exec -i docker_server-web_1 /var/www/html/behat/runBehatTests.sh

Les scripts setup.sh installent les paquets et préparent la base. La CD (livraison continue, la mise en production automatique) passe par AWS. Son pipeline a trois étapes : les tests, l'envoi de l'archive dans S3 (le stockage d'Amazon), l'installation sur le serveur par CodeDeploy (le service AWS qui déploie). Ce pipeline tourne en 18 minutes (leçon 59), puis l'auteur vérifie l'application à la main.

Mon avis, honnêtement

J'ai aimé voir un seul ticket traverser tous les niveaux de test. Le test qui plante parce qu'une exception n'est pas du bon type m'apprend plus sur le principe de Liskov qu'un schéma. L'auteur est crédible quand il avoue trois assertions dans un test : « il est préférable d'avoir une assertion par test » (leçon 48), puis il avance quand même.

Le reste sent la cave. L'image Docker est une release candidate (php:8.1.3RC1) et PHPUnit est en 9.5, avec des annotations dans des commentaires. Aucune syntaxe propre à PHP 8 n'apparaît. Seule la fonction str_contains, née avec PHP 8.0, s'y glisse (leçon 42) : « PHP 8 » dans le titre désigne surtout l'image Docker. Le pilote Goutte de Mink est abandonné (apport de ma part). Un « Laravel » traîne en fin de leçon 13 alors que le projet est Symfony.

Le pire : ToyCarCreator, la classe qui doit valider une voiture, appelle le validateur sans jamais lire sa réponse. Seule une exception levée par le validateur arrêterait le flux, et le test ne le vérifie jamais : son faux validateur répond toujours oui. Un fournisseur de données (la méthode qui alimente un test en jeux d'entrées) déclaré private complète le tableau. Il passe en 9.5, mais la documentation de PHPUnit veut des fournisseurs public (apport de ma part).

Mon verdict, 5,2 sur 10, vient de ce contraste : le fil BDD vers SOLID est bon, l'emballage a vieilli. Lisez les sections sur Behat et SOLID, survolez le reste, et gardez Beck et Zandstra à côté. Je veux maintenant la même chaîne avec les attributs de PHPUnit récent et l'autowiring partout.

Odilon

Toujours valable en 2026 ?

Les principes, oui. Les outils, en partie. D'après ma pratique, remplacez Goutte par le client HTTP de Symfony ou Panther (un outil qui pilote un vrai navigateur), docker-compose par docker compose, et les annotations par des attributs si vous êtes en PHPUnit 10 ou plus. Vérifiez chaque commande avant de la copier.

Pour qui ?

Lisez-le si

  • vous êtes dev PHP ou Symfony et n'avez jamais relié un ticket à un test automatisé.
  • vous voulez voir SOLID naître de tests rouges plutôt que d'un schéma, ou encadrez des juniors.
  • vous voulez essayer Behat et Gherkin avant de les proposer à une équipe.

Passez si

  • vous maîtrisez déjà le TDD : ce livre n'ajoute que Behat, SOLID et la CI.
  • vous cherchez du Symfony 7 moderne : le framework sert surtout de décor.
  • vous voulez du déploiement actuel : la partie AWS est un pas-à-pas daté.
Le lire sur ExpertEdge

Ce livre fait partie d'ExpertEdge, l'abonnement aux livres techniques de Packt. Le code ODILON50 donne 50 % de remise sur l'abonnement annuel : le livre sur ExpertEdge. Lien affilié : je touche une commission si vous vous abonnez avec ce code, et mes notes n'en dépendent pas.

Pour aller plus loin

Source : version cours ExpertEdge du livre, leçons 1 à 63 lues en détail, soit toutes celles extraites sur les 72 annoncées, quiz exclus. Année et pages issues de la fiche éditeur Packt.

Commentaires (0)

Voir toute la bibliothèque

D'autres fiches arrivent : un livre à la fois, la substantifique moelle seulement.