Travailler sur du code legacy

Nedry

Il y a quelques mois j’ai travaillé sur une codebase en PHP 5.4 d’environ 2 millions de lignes de code, sans documentation, sans test, en résumé du code legacy. Alors histoire d’avoir bien le contexte, quelques chiffres récupérés via PHPLOC :

  • 5944 classes
    • 40800 fonctions publiques
    • 8008 fonctions privées
  • 25.45 de complexité cyclomatique moyenne par classe
    • 10057 pour la classe la plus complexe
  • 4.05 de complexité cyclomatique moyenne par fonction
    • 1495 pour la fonction la plus complexe
  • 15% du code commenté

Pour en savoir plus sur la complexité cyclomatique d’une classe / fonction je vous invite à lire ce cours sur le site de l’Université de Marne-la-Vallée.

Dans ce genre de codebase, je ne suis pas très à l’aise. J’ai toujours peur qu’une modification réalisée à un endroit crée des bugs à un autre endroit. Même en étant rigoureux au niveau des tests manuels, je ne pense pas qu’on soit à l’abri de louper quelque chose. Bon au début ça allait. J’ai traité quelques tickets sur des scripts d’import / export de données avec des problèmes de performances, donc pas de dépendances et plutôt tranquille à gérer.

Mais à un moment, je suis arrivé sur ce que je redoutais, à savoir modifier une classe avec une dizaine de dépendances. Pour ne rien arranger, tester ça à la main prenait plusieurs minutes, au moins 10, l’enfer. La modification en elle-même était relativement simple à faire. Mais comment m’assurer que je n’allais rien casser ?

Freezé devant mon écran, avec un arbre de dépendances dessiné sur un brouillon et beaucoup trop de notes sur comment résoudre le problème, j’ai commencé à avoir un regain de lucidité et à écrire un test avec PHPUnit. Dans un monde idéal, j’aurais commencé par écrire ce test. Mais comme il y avait de nombreuses dépendances, que la classe que je cherchais à éditer était très liée à la base et à un état du système à un instant T, je me suis dit que ça allait être chaud. Mais en fait pas du tout.

J’ai commencé par formater la classe avec PHP CS Fixer. Parce qu’en plus d'être une codebase legacy la lisibilité c'était pas trop ça. Après j’ai réorganisé la classe en sortant toutes les dépendances dans des fonctions, afin de pouvoir les mocker par la suite. Les mocks, ce sont des objets simulés. En gros ça permet (lorsqu’on écrit des tests) de remplacer les sorties de certaines fonctions par ce que l’on souhaite.

Ensuite, j’ai récupéré les paramètres envoyés à ces dépendances afin de pouvoir appeler mes dépendances une par une pour en récupérer des sorties sérialisées, via la fonction PHP serialize. Enfin, j’ai utilisé ces sorties sérialisées dans mes mocks pour isoler mon test sur la fonction de la classe que je souhaitais tester. Ça donne quelque chose dans ce genre :

public function test_maFonctionDeMaClasse() {
    $mockedMethods = [
        'dependance_1',
        'dependance_2',
        'dependance_3',
        'dependance_4',
        'dependance_5',
        'dependance_6',
        'dependance_7',
        'dependance_8',
    ];

    // On instancie MaClasse avec les mocks listés au-dessus
    $maClasse = $this
        ->getMockBuilder('MaClasse')
        ->disableOriginalConstructor()
        ->setMethods($mockedMethods)
        ->getMock();

    // On configure les différents mocks
    foreach ($mockedMethods as $method) {
        $maClasse->expects($this->any())
            ->method($method)
            ->willReturn(unserialize(
                file_get_contents(
                    __DIR__.
                    '/serialized-outputs/maClasse/maFonction/Mocks/'.
                    $method.'.serialized'
                )
            ));
    }

    // On passe les paramètres envoyés aux dépendances
    $result = $maClasse->maFonction(
        1, 2, 3, 4, 'string', 5, 0, 0, null, null, null
    );

    // Le test
    $this->assertEquals(
        unserialize(
            file_get_contents(
                __DIR__.
                '/serialized-outputs/maClasse/maFonction/maFonction.serialized'
            )
        ),
        $result
    );
}

Et paf ça fait des chocapics ! J’avais maintenant un test m’assurant que tout marchait toujours comme avant et j’ai donc pu modifier ma classe l’esprit tranquille.

Cette solution n’est pas idéale, mais elle m’a permis de commencer à reprendre la main sur cette codebase et éventuellement d’envisager du refactoring. C’est déjà pas mal. Par contre attention, les mocks sont à utiliser avec modération. D’ailleurs si le sujet vous intéresse, je vous recommande de regarder cette conférence de Justin Searls (en anglais).

David Authier

David Authier
Développeur freelance