Aller au contenu
Sylvain Joffraud l4w
Retour

Un POC sans tests n’est pas un départ, c’est une direction

On hérite d’un POC. Un prototype qui « marche » : il déroule le parcours, il produit le bon résultat à l’écran, le client est content de la démo. La consigne initiale : « réutilisez ce qui existe, il ne reste qu’à finir ».

Audit du POC, première heure :

« Réutiliser » ça ? On a renégocié la consigne : partir des bonnes idées, refondre l’implémentation. La nuance n’est pas cosmétique — elle change tout le projet.

Un POC répond à « est-ce possible ? », pas à « est-ce maintenable ? »

Un POC est un dispositif de réponse à une question : le parcours est-il faisable dans les délais ? l’API du partenaire renvoie-t-elle ce qu’il faut ? le design tient-il ? Une fois la réponse obtenue (« oui »), le POC a fini son travail. Son code n’a jamais eu pour but de durer ; il a eu pour but de prouver.

Le piège, c’est de confondre le résultat du POC (l’intention validée : voilà le parcours, voilà les écrans, voilà les appels) avec son implémentation (un contrôleur de 545 lignes avec un token en dur). L’intention est précieuse, on la garde. L’implémentation est jetable, on la refond.

Concrètement, la refonte a suivi une trajectoire en deux temps : d’abord mettre sous tests le comportement existant (filet de sécurité), ensuite découper par extractions successives sans changer le comportement observable.

Découper un contrôleur obèse, une extraction à la fois

Un contrôleur de 600+ lignes ne se refond pas d’un bloc — on le vide par petites MR, chacune extrayant une responsabilité vers un objet testable hors du framework. Exemple : la normalisation de téléphone, un helper privé noyé dans le contrôleur, devient un Value Object immutable.

// AVANT — méthode privée dans un contrôleur de 662 lignes, non testable seule
private function normalizePhone(string $raw): ?string
{
    // 15 lignes de strip + regex + préfixage…
}

// APRÈS — Value Object isolé, testé par 25 cas, contrôleur allégé de 19 lignes
$phone = PhoneE164::tryFromUserInput($input)?->value;   // null si invalide → flash

Même mécanique pour la matrice de pièces justificatives obligatoires : 25 lignes de règles métier (« quelles sections sont requises selon le profil de l’adhérent ») extraites du contrôleur vers un service pur, dont la logique de calcul devient enfin testable en isolation :

final class UploadValidator
{
    /** @return list<string> les sections obligatoires selon le contexte */
    public function computeRequiredSections(
        bool $contratActuel,
        bool $hasConjoint,
        array $enfants,
    ): array {
        $sections = [
            self::SECTION_IDENTITE,
            self::SECTION_ATTESTATION,
            self::SECTION_NAISSANCE,
            self::SECTION_RIB,
        ];

        if ($contratActuel) {
            $sections[] = self::SECTION_RADIATION;
        }
        if ($hasConjoint) {
            $sections[] = self::SECTION_CONJOINT_SITUATION;
            $sections[] = self::SECTION_CONJOINT_ATTESTATION;
        }
        foreach ($enfants as $i => $enfant) {
            $sections[] = "enfant_{$i}_attestation";
            if ($enfant->scolariteRequise()) {           // 18-28 ans
                $sections[] = "enfant_{$i}_scolarite";
            }
        }

        return $sections;
    }
}

Chaque MR de ce type : −20 à −30 lignes dans le contrôleur, +10 à +25 tests ciblés, 0 régression. Cumulées, une dizaine de ces extractions ont fait fondre le contrôleur principal de 686 à 612 lignes tout en transformant des helpers privés inaccessibles en services couverts. Et en chemin, on a posé des couches DDD light (Domain / Application / Infrastructure), des interfaces sur les clients tiers (DIP), des enums PHP 8.1 à la place des public const à gogo.

Le garde-fou qui rend tout ça possible : la CI dès la première MR

Rien de ce qui précède n’est sûr sans filet. La toute première chose mise en place, avant même la première extraction, c’est un pipeline CI vert obligatoire sur chaque MR : tests + analyse statique + style. Ça paraît prématuré sur un projet qui n’a aucun test — c’est exactement l’inverse. Elle impose la discipline pour toutes les MR suivantes : impossible de merger une extraction qui casse un comportement, parce que le test de régression écrit avant l’extraction l’attrape.

Quelques mois plus tard, le bilan :

Lessons learned

#tdd #refactoring #ddd #legacy #symfony #architecture


Partager cet article :

Article précédent
Mocker `navigator.wakeLock` readonly : defineProperty
Article suivant
Edge marker Leaflet : caché sous les contrôles, intappable