uk flag

Exemple de refactorisation - Partie 1 - Rendre le code testable

Nous allons voir ensemble un exemple de refactorisation. Notre objectif est de retravailler ce code pour en améliorer sa maintenabilité. Pour pouvoir suivre cette série, vous devez être familié avec les tests unitaires, j'utiliserai le framework PHPUnit.

Voici ci-dessous le script Client.php et la classe UserManager que nous allons refactoriser:

// Client.php

$api = new Api();
$data = $api->getUserData();

$userManager = new UserManager();
$userManager->createUser($data);
// UserManager.php

class UserManager
{
    public function createUser(array $data)
    {
        $data['firstName'] = mb_strtolower($data['firstName']);
        $data['lastName'] = mb_strtolower($data['lastName']);
        $data['birthdate'] = $data['birthYear'] .'-'. $data['birthMonth'] .'-'. $data['birthDay'];
        unset($data['birthYear']);
        unset($data['birthMonth']);
        unset($data['birthDay']);
        $repository = new DatabaseRepository();
        $repository->saveUser($data);
        $emailClient = new EmailClient();
        $msg = 'a new user has been created';
        $emailClient->sendEmail('admin@example.com','A new user has been created', $msg);
    }
}

Avant de faire de la refactorisation, la classe UserManager doit avoir des tests unitaires pour pouvoir effectuer des modifications en toute confiance, sans avoir peur de casser quelque chose. Malheureusement, les tests unitaires sont inexistants, et encore pire, la classe UserManager n'a pas été développée pour être testable:

Si une classe dépendante est simplement instanciée dans le code de la méthode à tester comme c'est le cas pour DatabaseRepository et EmailClient (ligne 14 et ligne 16), il ne sera pas possible de la simuler en la remplaçant par une doublure de test. Pour chaque exécution du test, il y aura donc un enregistrement dans la base de donnée et un email sera envoyé.

Je vais refactoriser le code pour pouvoir utiliser dans la classe Usermanager les objets DatabaseRepository et EmailClient via le mécanisme d'injection de dépendances. Je pourrai alors remplacer ces objets par des doublures lors de mes tests.

// Client.php
$api = new Api();
$d = $api->getUserData();

$repository = new DatabaseRepository();
$emailClient = new EmailClient();
$userManager = new UserManager($repository, $emailClient);
$userManager->save($d);
// UserManager.php
class UserManager
{
    private $databaseRepository;

    private $emailClient;

    public function __construct(DatabaseRepository $databaseRepository, EmailClient $emailClient)
    {
        $this->databaseRepository = $databaseRepository;
        $this->emailClient = $emailClient;
    }

    public function createUser(array $data)
    {
        $data['firstName'] = mb_strtolower($data['firstName']);
        $data['lastName'] = mb_strtolower($data['lastName']);
        $data['birthdate'] = $data['birthYear'] .'-'. $data['birthMonth'] .'-'. $data['birthDay'];
        unset($data['birthYear']);
        unset($data['birthMonth']);
        unset($data['birthDay']);
        $repository = new DatabaseRepository();
        $this->databaseRepository->saveUser($data);
        $emailClient = new EmailClient();
        $msg = 'a new user has been created';
        $this->emailClient->sendEmail('admin@example.com','A new user has been created', $msg);
    }
}

Pour les besoins du futur test, je vais tout d'abord récuperer des données de l'api utilisée par Client.php en effectuant un simple var_dump de la variable $data. Je peux ensuite créer un squelette de test unitaire pour la méthode createUser:

// UserManagerTest.php
class UserManagerTest extends \PHPUnit\Framework\TestCase
{
    public function testCreateUser()
    {
        $data = [
           'firstName' => 'John',
           'lastName' => 'Doe',
           'title' => 'Mr',
           'birthYear' => '1981',
           'birthMonth' => '06',
           'birthDay' => '04',
        ];

        $databaseRepositoryDouble = $this->getMockBuilder(DatabaseRepository::class)->getMock();
        $emailClientDouble = $this->getMockBuilder(EmailClient::class)->getMock();

        $userManager = new UserManager($databaseRepositoryDouble, $emailClientDouble);
        $userManager->createUser($data);
    }
}

Lorsque j'exécute le test j'ai le résultat suivant:

This test did not perform any assertions
OK, but incomplete, skipped, or risky tests!
Tests: 1, Assertions: 0, Risky: 1.

Le premier objectif est réalisé :-), je peux désormais exécuter autant de fois que voulu le test sans effectuer d'enregistrements dans la base de donnée ou envoyer d'emails.

Lorsque l'on touche à du code legacy, il est très souvent necessaire d'effectuer des modifications du code avant de pouvoir effectuer des tests pour ensuite pouvoir effectuer des changements. Michael C. Feathers l'exprime de cette façon dans son livre Working effectively with legacy code qui est une référence sur le sujet: When we change code, we should have tests in place. To put tests in place, we often have to change code.

Dans le prochain article je vais créer les tests unitaires de la classe UserManager :-D.

Exemple de refactorisation - Partie 2 - Créer les tests unitaires