Chapitre 12. Développement dirigé par les tests

Les tests unitaires sont une partie vitale pour plusieurs pratiques et processus de développement logiciel tel que la programmation en testant d'abord, l'Extreme Programming, et le développement dirigé par les tests. Ils permettent également la conception par contrat dans des langages de programmation qui ne supportent pas cette méthodologie par construction de langage.

Vous pouvez utiliser PHPUnit pour écrire des tests une fois que vous avez fait la programmation. Cependant, plus tôt un test est écrit après qu'une erreur a été introduite, plus le test a de la valeur. Ainsi, au lieu d'écrire des tests des mois après que le code est "achevé", nous pouvons écrire des tests quelques jours, heures ou minutes après la possible introduction d'un défaut. Pourquoi s'arrêter là ? Pourquoi ne pas écrire les tests un peu avant la possible introduction d'un défaut ?

La programmation en testant d'abord, qui est une partie de l'Extreme Programming et le développement dirigé par les tests, sont bâtis sur cette idée et la poussent à l'extrême. Grâce à la puissance de calcul actuelle, nous avons l'opportunité de lancer des milliers de tests des milliers de fois par jour. Nous pouvons utiliser les retours de tous ces tests pour programmer par petites étapes, chacune d'elles apportant avec elle l'assurance d'un nouveau test automatisé s'ajoutant à tous les tests venus précédemment. Les tests sont comme des pitons, vous assurant que, quoi qu'il arrive, une fois que vous avez progressé, vous ne pouvez pas retomber plus bas.

Quand vous écrivez le test la première fois, il ne peut pas être exécuté, car vous faites appel à des objets et des méthodes qui n'ont pas encore été programmés. Ceci peut sembler étrange au premier abord, mais après un moment, vous aurez l'habitude de procéder ainsi. Pensez à la programmation en testant d'abord comme à une approche pragmatique pour suivre le principe de programmation orientée objet consistant à programmer une interface au lieu de programmer une implémentation : quand vous écrivez le test, vous pensez à l'interface de l'objet que vous êtes en train de tester - ce à quoi ressemble cet objet vu de l'extérieur. Quand vous faites en sorte que le test fonctionne vraiment, vous réfléchissez en terme de pure implémentation. L'interface est déterminée par le test en échec.

 

L'objet du Développement dirigé par les tests est de rechercher les fonctionnalités dont le logiciel a réellement besoin, plutôt que celles dont le programmeur pense qu'il pourrait probablement avoir besoin. La façon dont il procède semble d'abord contre intuitive, si ce n'est carrément idiote, mais non seulement cela a du sens, mais cela devient également rapidement une façon naturelle et élégante de développer du logiciel.

 
 --Dan North

Ce qui suit est forcément une introduction abrégée au développement dirigé par les tests. Vous pouvez approfondir le sujet dans d'autres livres, comme Test-Driven Development [Beck2002] de Kent Beck ou A Practical Guide to Test-Driven Development [Astels2003] de Dave Astels.

Exemple du compte bancaire

Dans cette section, nous examinerons l'exemple d'une classe qui représente un compte bancaire. Le contrat pour la classe CompteBancaire n'exige pas seulement des méthodes pour obtenir et positionner la balance du compte bancaire, ainsi que des méthodes pour déposer et retirer de l'argent. S'y ajoute les deux conditions suivantes qui doivent être vérifiées :

  • La balance initiale du compte bancaire doit être à zéro.

  • La balance du compte bancaire ne peut pas devenir négative.

Nous écrivons les tests pour la classe CompteBancaire avant d'écrire le code de la classe elle-même. Nous utilisons les conditions du contrat comme base pour les tests et nous nommons les méthodes de test en fonction, comme montré dans Exemple 12.1, « Tests pour la classe CompteBancaire ».

Exemple 12.1. Tests pour la classe CompteBancaire

<?php
require_once 'CompteBancaire.php';

class CompteBancaireTest extends PHPUnit_Framework_TestCase
{
    protected $compte_bancaire;

    protected function setUp()
    {
        $this->compte_bancaire = new CompteBancaire;
    }

    public function testBalanceEstInitialementAZero()
    {
        $this->assertEquals(0, $this->compte_bancaire->getBalance());
    }

    public function testBalanceNePeutPasEtreNegatif()
    {
        try {
            $this->compte_bancaire->retirerArgent(1);
        }

        catch (CompteBancaireException $e) {
            $this->assertEquals(0, $this->compte_bancaire->getBalance());

            return;
        }

        $this->fail();
    }

    public function testBalanceNePeutPasEtreNegatif2()
    {
        try {
            $this->compte_bancaire->deposerArgent(-1);
        }

        catch (CompteBancaireException $e) {
            $this->assertEquals(0, $this->compte_bancaire->getBalance());

            return;
        }

        $this->fail();
    }
}
?>


Nous écrivons maintenant le volume minimum de code pour que le premier test, testBalanceEstInitialementAZero(), réussisse. Dans notre exemple, ceci correspond à implémenter la méthode getBalance() de la classe CompteBancaire, comme montré dans Exemple 12.2, « Code nécessaire pour que le test testBalanceEstInitialementAZero() réussisse ».

Exemple 12.2. Code nécessaire pour que le test testBalanceEstInitialementAZero() réussisse

<?php
class CompteBancaire
{
    protected $balance = 0;

    public function getBalance()
    {
        return $this->balance;
    }
}
?>


Maintenant, le test pour la première condition du contrat réussit, mais les tests pour la seconde condition du contrat échoue car nous n'avons pas implémenté les méthodes que ces tests appellent.

phpunit CompteBancaireTest
PHPUnit 3.7.0 by Sebastian Bergmann.

.
Fatal error: Call to undefined method CompteBancaire::retirerArgent()

Pour que les tests qui s'assurent que la seconde condition du contrat réussissent, nous devons maintenant implémenter les méthodes retirerArgent(), deposerArgent() et setBalance(), comme montré dans Exemple 12.3, « La classe CompteBancaire complète ». Ces méthodes sont écrites de telle façon qu'elles lèvent une CompteBancaireException quand elles sont appelées avec des valeurs illégales qui violeraient les conditions du contrat.

Exemple 12.3. La classe CompteBancaire complète

<?php
class CompteBancaireException extends Exception { }

class CompteBancaire
{
    protected $balance = 0;

    public function getBalance()
    {
        return $this->balance;
    }

    protected function setBalance($balance)
    {
        if ($balance >= 0) {
            $this->balance = $balance;
        } else {
            throw new CompteBancaireException;
        }
    }

    public function deposerArgent($balance)
    {
        $this->setBalance($this->getBalance() + $balance);

        return $this->getBalance();
    }

    public function retirerArgent($balance)
    {
        $this->setBalance($this->getBalance() - $balance);

        return $this->getBalance();
    }
}
?>


Les tests qui assurent que la seconde condition du contrat réussissent maintenant aussi :

phpunit CompteBancaireTest
PHPUnit 3.7.0 by Sebastian Bergmann.

...

Time: 0 seconds


OK (3 tests, 3 assertions)

Alternativement, vous pouvez utiliser les méthodes statiques de vérification fournies par la classe PHPUnit_Framework_Assert pour écrire les conditions du contrat en tant que vérification en style "conception par contrat", comme montré dans Exemple 12.4, « La classe CompteBancaire avec des vérifications de conception par contrat ». Quand l'une de ces vérifications échoue, une exception PHPUnit_Framework_AssertionFailedError sera levée. Avec cette approche, vous écrivez moins de code pour les contrôles des conditions du contrat et les tests deviennent plus lisibles. Cependant, vous ajoutez une dépendance à l'exécution avec PHPUnit à vos projets.

Exemple 12.4. La classe CompteBancaire avec des vérifications de conception par contrat

<?php
class CompteBancaire
{
    private $balance = 0;

    public function getBalance()
    {
        return $this->balance;
    }

    protected function setBalance($balance)
    {
        PHPUnit_Framework_Assert::assertTrue($balance >= 0);

        $this->balance = $balance;
    }

    public function deposerArgent($montant)
    {
        PHPUnit_Framework_Assert::assertTrue($montant >= 0);

        $this->setBalance($this->getBalance() + $montant);

        return $this->getBalance();
    }

    public function retirerArgent($montant)
    {
        PHPUnit_Framework_Assert::assertTrue($montant >= 0);
        PHPUnit_Framework_Assert::assertTrue($this->balance >= $montant);

        $this->setBalance($this->getBalance() - $montant);

        return $this->getBalance();
    }
}
?>


En écrivant les conditions du contrat dans les tests, nous avons utilisé la conception par contrat pour programmer la classe CompteBancaire. Nous avons alors écrit, suivant l'approche de la programmation en testant d'abord, le code nécessaire pour faire que les tests réussissent. Cependant, nous avons oublié d'écrire les tests qui appellent setBalance(), deposerArgent() et retirerArgent() avec des valeurs valides qui ne violent pas les conditions du contrat. Nous avons besoin d'un moyen pour tester nos tests, ou au moins mesurer leur qualité. Un tel moyen est l'analyse de l'information de couverture de code que nous allons voir.

Please open a ticket on GitHub to suggest improvements to this page. Thanks!