Chapitre 10. Analyse de couverture de code

 

La beauté du test ne se trouve pas dans l'effort mais dans l'efficience.

Savoir ce qui doit être testé est magnifique, et savoir ce qui est testé est magnifique.

 
 --Murali Nandigama

Dans ce chapitre, vous apprendrez tout sur la fonctionnalité de couverture de code de PHPUnit qui fournit une vision interne des parties du code de production qui sont exécutées quand les tests sont exécutés. Cela aide à répondre à des questions comme :

Un exemple de ce que peuvent signifier des statistiques de couverture de code est, s'il y a une méthode avec 100 lignes de code, et seulement 75 de ces lignes sont réellement exécutées quand les tests sont lancés, alors la méthode est considérée comme ayant une couverture de code de 75 pour cent.

La fonctionnalité de couverture de code de PHPUnit fait usage du composant PHP_CodeCoverage qui, à son tour, tire partie de la fonctionnalité de couverture d'instructions fournie par l'extension Xdebug de PHP.

Générons un rapport de couverture de code pour la classe CompteBancaire de ???.

phpunit --coverage-html ./rapport CompteBancaireTest
PHPUnit 4.2.0 by Sebastian Bergmann.

...

Time: 0 seconds

OK (3 tests, 3 assertions)

Generating report, this may take a moment.

Figure 10.1, « Couverture de code pour setBalance() » montre un extrait du rapport de couverture de code. Les lignes de code qui ont été exécutés pendant le fonctionnement des tests sont surlignés en vert, les lignes de code qui sont exécutables mais n'ont pas été exécutées sont surlignées en rouge et le "code mort" est surligné en gris. Le nombre à gauche du numéro de la ligne de code indique combien de tests couvrent cette ligne.

Figure 10.1. Couverture de code pour setBalance()

Couverture de code pour setBalance()


Cliquer sur le numéro de ligne d'une ligne couverte ouvrira un panneau (voir Figure 10.2, « Panneau avec l'information des tests couvrant la ligne ») qui montre les cas de test qui couvrent cette ligne.

Figure 10.2. Panneau avec l'information des tests couvrant la ligne

Panneau avec l'information des tests couvrant la ligne


Le rapport de couverture de code de notre exemple CompteBancaire montre que nous n'avons actuellement aucun test qui appellent les méthodes setBalance(), deposerArgent() et retirerArgent() avec des valeurs acceptables. Exemple 10.1, « Test manquant pour atteindre la couverture de code complète » montre un test qui peut être ajouté à la classe de cas de test BankAccountTest pour couvrir complètement la classe CompteBancaire.

Exemple 10.1. Test manquant pour atteindre la couverture de code complète

<?php
require_once 'CompteBancaire.php';

class BankAccountTest extends PHPUnit_Framework_TestCase
{
    // ...

    public function testDeposerRetirerArgent()
    {
        $this->assertEquals(0, $this->compte_bancaire->getBalance());
        $this->compte_bancaire->deposerArgent(1);
        $this->assertEquals(1, $this->compte_bancaire->getBalance());
        $this->compte_bancaire->retirerArgent(1);
        $this->assertEquals(0, $this->compte_bancaire->getBalance());
    }
}
?>


Figure 10.3, « Couverture de code pour setBalance() avec un test additionnel » montre la couverture de code de la méthode setBalance() avec le test additionnel.

Figure 10.3. Couverture de code pour setBalance() avec un test additionnel

Couverture de code pour setBalance() avec un test additionnel


Spécifier les méthodes couvertes

L'annotation @covers (voir Tableau A.1, « Annotations pour indiquer quelles méthodes sont couvertes par un test ») peut être utilisée dans le code de test pour indiquer quelle(s) méthode(s) une méthode de test veut test. Si elle est fournie, seules les informations de couverture de code pour la(les) méthode(s) indiquées seront prises en considération. Exemple 10.2, « Tests qui indiquent quelle(s) méthode(s) ils veulent couvrir » montre un exemple.

Exemple 10.2. Tests qui indiquent quelle(s) méthode(s) ils veulent couvrir

<?php
require_once 'CompteBancaire.php';

class CompteBancaireTest extends PHPUnit_Framework_TestCase
{
    protected $compte_bancaire;

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

    /**
     * @covers CompteBancaire::getBalance
     */
    public function testBalanceEstInitialementZero()
    {
        $this->assertEquals(0, $this->compte_bancaire->getBalance());
    }

    /**
     * @covers CompteBancaire::retirerArgent
     */
    public function testBalanceNePeutPasDevenirNegative()
    {
        try {
            $this->compte_bancaire->retirerArgent(1);
        }

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

            return;
        }

        $this->fail();
    }

    /**
     * @covers CompteBancaire::deposerArgent
     */
    public function testBalanceNePeutPasDevenirNegative2()
    {
        try {
            $this->compte_bancaire->deposerArgent(-1);
        }

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

            return;
        }

        $this->fail();
    }

    /**
     * @covers CompteBancaire::getBalance
     * @covers CompteBancaire::deposerArgent
     * @covers CompteBancaire::retirerArgent
     */

    public function testDeposerArgent()
    {
        $this->assertEquals(0, $this->compte_bancaire->getBalance());
        $this->compte_bancaire->deposerArgent(1);
        $this->assertEquals(1, $this->compte_bancaire->getBalance());
        $this->compte_bancaire->retirerArgent(1);
        $this->assertEquals(0, $this->compte_bancaire->getBalance());
    }
}
?>


Il est également possible d'indiquer qu'un test ne doit couvrir aucune méthode en utilisant l'annotation @coversNothing (voir la section intitulée « @coversNothing »). Ceci peut être utile quand on écrit des tests d'intégration pour s'assurer que vous ne générez une couverture de code avec des tests unitaires.

Exemple 10.3. Un test qui indique qu'aucune méthode ne doit être couverte

<?php
class IntegrationLivreDOrTest extends PHPUnit_Extensions_Database_TestCase
{
    /**
     * @coversNothing
     */
    public function testAjouteEntree()
    {
        $livre_d_or = new LivredOr();
        $livre_d_or->addEntry("suzy", "Hello world!");

        $queryTable = $this->getConnection()->createQueryTable(
            'livre_d_or', 'SELECT * FROM livre_d_or'
        );
        $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml")
                              ->getTable("livre_d_or");
        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}
?>
      


Ignorer des blocs de code

Parfois, vous avez des blocs de code que vous ne pouvez pas tester et que voulez ignorer lors de l'analyse de couverture de code. PHPUnit vous permet de faire cela en utilisant les annotations @codeCoverageIgnore, @codeCoverageIgnoreStart et @codeCoverageIgnoreEnd comme montré dans Exemple 10.4, « Utiliser les annotations @codeCoverageIgnore, @codeCoverageIgnoreStart et @codeCoverageIgnoreEnd ».

Exemple 10.4. Utiliser les annotations @codeCoverageIgnore, @codeCoverageIgnoreStart et @codeCoverageIgnoreEnd

<?php
/**
 * @codeCoverageIgnore
 */
class Foo
{
    public function bar()
    {
    }
}

class Bar
{
    /**
     * @codeCoverageIgnore
     */
    public function foo()
    {
    }
}

if (FALSE) {
    // @codeCoverageIgnoreStart
    print '*';
    // @codeCoverageIgnoreEnd
}
?>


Les lignes de code qui sont marquées comme devant être ignorées en utilisant les annotations sont comptées comme exécutées (si elles sont exécutables) et ne seront pas surlignées.

Inclure et exclure des fichiers

Par défaut, tous les fichiers de code source qui contiennent au moins une ligne de code qui a été exécutée (et seulement ces fichiers) sont inclus dans le rapport. Les fichiers de code source qui sont inclus dans le rapport peuvent être filtrés en utilisant une approche par liste noire ou liste blanche.

La liste noire est pré-remplie avec tous les fichiers de code source de PHPUnit lui-même ainsi que les tests. Quand la liste blanche est vide (par défaut), le filtrage par liste noire est utilisé. Quand la liste blanche n'est pas vide, le filtrage par liste blanche est utilisé. Chaque fichier de la liste blanche est ajouté au rapport de couverture de code, qu'il ait été exécuté ou pas. Toutes les lignes d'un tel fichier, incluant celles qui ne sont pas exécutables, sont comptées comme non exécutées.

Quand vous configurez processUncoveredFilesFromWhitelist="true" dans votre configuration PHPUnit (voir la section intitulée « Inclure et exclure des fichiers de la couverture de code ») alors ces fichiers seront à inclure par PHP_CodeCoverage pour calculer correctement le nombre de lignes exécutables.

Note

Merci de noter que le chargement des fichiers de code source réalisé quand processUncoveredFilesFromWhitelist="true" est positionné, peut poser des problèmes quand un fichier de code source contient du code hors de la portée d'une classe ou d'une fonction, par exemple.

Le fichier de configuration XML de PHPUnit (voir la section intitulée « Inclure et exclure des fichiers de la couverture de code ») peut être utilisé pour contrôler les listes noires et blanches. Utiliser une liste blanche est recommandé comme meilleure pratique pour contrôler la liste des fichiers inclus dans le rapport de couverture de code.

Cas limites

Dans la plupart des cas, on peut dire sans risque que PHPUnit offre une information de couverture de code "basée sur les lignes" mais du fait de la façon dont l'information est collectée, il existe quelques cas limites qui valent la peine d'être mentionnés.

Exemple 10.5. 

<?php
// Parce qu'il s'agit d'une couverture "basée sur les lignes" et pas sur les instructions
// une ligne aura toujours un état de couverture donné
if(false) cet_appel_de_fonction_sera_compte_comme_couvert();

// Du fait de la façon dont la couverture de code fonctionne en interne, ces deux lignes sont spéciales.
/ Cette ligne sera comptée comme non exécutable
if(false)
    // Cette ligne sera comptée comme couverte car c'est en fait la 
    // couverture de l'instruction if dans la ligne au-dessus qui 
    // sera montrée ici !
    sera_egalement_comptee_comme_couverte();

// Pour éviter cela, il est nécessaire d'utiliser des accolades
if(false) {
    cet_appel_ne_sera_jamais_compte_comme_couvert();
}
?>