Leçon 8 / 20

Tester les parcours fonctionnels

45 min PHP 8 — architecture, tests et sécurité Gratuit

Objectif : Vérifier les réponses HTTP et les effets en base lors de l’ajout et de la modification d’un contact.

Un service peut réussir ses tests isolés alors que le parcours complet échoue : route mal raccordée, formulaire incorrect ou écriture non transmise à la base. Vous allez donc tester le carnet par HTTP et vérifier que l’ajout puis la modification d’un contact produisent les résultats attendus dans une base réservée aux tests.

Les tests précédents vérifient les règles de ContactService et ses interactions avec une doublure de DepotInterface. Ici, vous testez l’application assemblée : le contrôleur reçoit une requête HTTP, les composants réels sont utilisés et la persistance est observée dans la base de test. Le test ne remplace donc pas les tests unitaires ; il complète leur couverture.

Préparer un environnement isolé

Un test fonctionnel envoie une requête à l’application démarrée comme un serveur, puis examine sa réponse et ses effets observables. Pour qu’il reste reproductible, ne pointez jamais vers la base utilisée par le carnet réel. Configurez une base distincte, créée avec le schéma du projet, et un mot de passe de test connu de l’application.

Le code ci-dessous suppose que l’application est accessible à http://127.0.0.1:8080, que la connexion se fait sur /connexion et que la gestion des contacts utilise /contacts. Lancez le serveur avec la configuration de test et fournissez les variables d’environnement indiquées. Adaptez les noms de variables d’environnement aux mécanismes de configuration déjà présents, sans modifier les fichiers du projet pour les besoins du test.

Écrire le parcours HTTP

Dans le test, un petit client HTTP transmet les formulaires et conserve le cookie de session reçu à la connexion. La page de connexion fournit le champ caché attendu pour les formulaires ; le test le récupère afin de respecter le fonctionnement de l’application, sans réimplémenter sa sécurité. Les requêtes d’ajout et de modification doivent inclure ce champ.

tests/Fonctionnel/CarnetFonctionnelTest.phpPHP
<?php

declare(strict_types=1);

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class CarnetFonctionnelTest extends TestCase
{
    private string $baseUrl;
    private string $cookie = '';
    private string $jetonCsrf = '';
    private PDO $pdo;

    protected function setUp(): void
    {
        $this->baseUrl = getenv('CARNET_TEST_URL') ?: 'http://127.0.0.1:8080';
        $dsn = getenv('CARNET_TEST_DSN');
        $motDePasse = getenv('CARNET_TEST_MOT_DE_PASSE');

        if ($dsn === false || $motDePasse === false) {
            self::fail('Configurez CARNET_TEST_DSN et CARNET_TEST_MOT_DE_PASSE.');
        }

        $this->pdo = new PDO($dsn, getenv('CARNET_TEST_DB_USER') ?: '', getenv('CARNET_TEST_DB_PASSWORD') ?: '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        ]);
        $this->pdo->exec('DELETE FROM contacts');

        $connexion = $this->requete('GET', '/connexion');
        self::assertSame(200, $connexion['statut']);
        preg_match('/name="jeton_csrf" value="([^"]+)"/', $connexion['corps'], $correspondance);
        self::assertNotEmpty($correspondance[1] ?? null, 'Le formulaire de connexion doit fournir son champ attendu.');
        $this->jetonCsrf = $correspondance[1];

        $reponse = $this->requete('POST', '/connexion', [
            'mot_de_passe' => $motDePasse,
            'jeton_csrf' => $this->jetonCsrf,
        ]);
        self::assertContains($reponse['statut'], [200, 302]);
        self::assertNotSame('', $this->cookie, 'La connexion doit établir une session.');
    }

    #[Test]
    public function unContactPeutEtreAjoutePuisModifie(): void
    {
        $ajout = $this->requete('POST', '/contacts', [
            'jeton_csrf' => $this->jetonCsrf,
            'nom' => 'Camille Martin',
            'courriel' => '[email protected]',
            'telephone' => '0102030405',
        ]);
        self::assertContains($ajout['statut'], [200, 302]);

        $contact = $this->pdo->query(
            "SELECT id, nom, courriel, telephone FROM contacts WHERE courriel = '[email protected]'"
        )->fetch(PDO::FETCH_ASSOC);
        self::assertIsArray($contact, 'Le contact ajouté doit être présent dans la base de test.');
        self::assertSame('Camille Martin', $contact['nom']);

        $modification = $this->requete('POST', '/contacts', [
            'jeton_csrf' => $this->jetonCsrf,
            'id' => (string) $contact['id'],
            'nom' => 'Camille Martin-Durand',
            'courriel' => '[email protected]',
            'telephone' => '0607080910',
        ]);
        self::assertContains($modification['statut'], [200, 302]);

        $verifie = $this->pdo->query(
            "SELECT nom, telephone FROM contacts WHERE id = " . (int) $contact['id']
        )->fetch(PDO::FETCH_ASSOC);
        self::assertSame('Camille Martin-Durand', $verifie['nom'] ?? null);
        self::assertSame('0607080910', $verifie['telephone'] ?? null);
    }

    private function requete(string $methode, string $chemin, array $donnees = []): array
    {
        $options = [
            'http' => [
                'method' => $methode,
                'ignore_errors' => true,
                'header' => "Content-Type: application/x-www-form-urlencoded\r\n"
                    . ($this->cookie === '' ? '' : 'Cookie: ' . $this->cookie . "\r\n"),
                'content' => http_build_query($donnees),
            ],
        ];
        $corps = file_get_contents($this->baseUrl . $chemin, false, stream_context_create($options));
        self::assertNotFalse($corps, 'Le serveur de test ne répond pas.');
        $entetes = $http_response_header ?? [];
        preg_match('/HTTP\/\S+\s+(\d+)/', $entetes[0] ?? '', $statut);

        foreach ($entetes as $entete) {
            if (stripos($entete, 'Set-Cookie:') === 0) {
                $this->cookie = explode(';', substr($entete, 12), 2)[0];
            }
        }

        return ['statut' => (int) ($statut[1] ?? 0), 'corps' => $corps];
    }
}

La requête SQL de vérification est exécutée par le test directement sur la base dédiée ; elle ne remplace pas le dépôt de l’application. Les valeurs fixes de cet exemple servent à retrouver le résultat créé par ce test. Dans un environnement partagé entre plusieurs exécutions, prévoyez un nettoyage ciblé des seules données de test et évitez toute suppression globale.

Exécuter et interpréter les tests

Démarrez d’abord le serveur avec la base de test, puis lancez PHPUnit depuis la racine du projet. Les identifiants de base et le mot de passe sont fournis à l’exécution, pas inscrits dans le dépôt. La commande cible uniquement le parcours fonctionnel présenté.

ExécutionTerminal
CARNET_TEST_URL=http://127.0.0.1:8080 \
CARNET_TEST_DSN='mysql:host=127.0.0.1;dbname=carnet_test;charset=utf8mb4' \
CARNET_TEST_DB_USER=carnet_test \
CARNET_TEST_DB_PASSWORD=secret \
CARNET_TEST_MOT_DE_PASSE=mot-de-passe-test \
vendor/bin/phpunit tests/Fonctionnel/CarnetFonctionnelTest.php
Résultat attendu
OK (1 test, plusieurs assertions)

Une réponse HTTP réussie ne suffit pas à prouver qu’une mutation a fonctionné : le test vérifie aussi les valeurs relues en base. Inversement, une écriture correcte ne suffit pas si l’application renvoie une erreur ou refuse le parcours. Si vos routes renvoient un statut précis, remplacez les assertions permissives par ce statut attendu et vérifiez également le contenu de la réponse lorsque cela apporte une garantie utile.

À retenir

  • Un test fonctionnel traverse l’application assemblée au moyen de requêtes HTTP réelles.
  • Vérifiez les statuts de réponse et les effets persistés ; l’un ne prouve pas automatiquement l’autre.
  • Utilisez une base dédiée et des données isolées, avec un nettoyage maîtrisé.
  • Conservez les tests unitaires pour localiser rapidement les erreurs de règles métier.

La suite du cours sécurisera les requêtes PDO utilisées par le dépôt avant de traiter les autres protections du carnet.

Envie d'aller plus loin ?

Les cours Enovatic sont gratuits et le resteront. Si vous cherchez un accompagnement encadré, nos formations professionnelles sont animées par les mêmes formateurs.