10. Analyse de couverture de code

Wikipedia:

En informatique, la couverture de code est une mesure utilisée pour décrire le taux de code source testé d’un programme lorsqu’il est testé par une suite de test particulière. Un programme avec un taux de couverture de code élevée a été plus complètement testé et a une plus faible chance de contenir des bugs logiciel qu’un programme avec un taux de couverture de code faible.

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. Elle utilise le composant php-code-coverage qui tire parti de la fonctionnalité de couverture de code fournie par l’extension Xdebug de PHP.

Note

Xdebug n’est pas distribué au sein de PHPUnit. Si une notice indiquant qu’aucun pilote de couverture de code n’est disponible en lançant les tests, cela signifie que Xdebug n’est pas installé ou n’est pas configuré correctement. Avant de pouvoir utiliser les fonctionnalités de couverture de code dans PHPUnit, vous devez lire le guide d’installation de Xdebug..

php-code-coverage prend également en charge phpdbg comme source alternative pour les données de couverture de code.

PHPUnit peut générer un rapport de couverture de code HTML aussi bien que des fichiers de log en XML avec les informations de couverture de code dans différents formats (Clover, Crap4J, PHPUnit). Les informations de couverture de code peuvent aussi être rapportées en text (et affichées vers STDOUT) et exportées en PHP pour un traitement ultérieur.

Reportez-vous à Le lanceur de tests en ligne de commandes pour obtenir la liste des options de ligne de commande qui contrôlent la fonctionnalité de couverture de code ainsi que Journalisation pour les paramètres de configuration appropriés.

Indicateurs logiciels pour la couverture de code

Différents indicateurs logiciels existent pour mesurer la couverture de code :

Couverture de ligne

L’indicateur logiciel de couverture de ligne mesure si chaque ligne exécutable a été exécutée.

Couverture de fonction et de méthode

L’indicateur logiciel de couverture de fonction et de méthode mesure si chaque fonction ou méthode a été invoquée. php-code-coverage considère qu’une fonction ou une méthode a été couverte seulement quand toutes ses lignes exécutables sont couvertes.

Couverture de classe et de trait

L’indicateur logiciel de couverture de classe et de trait mesure si chaque méthode d’une classe ou d’un trait est couverte. php-code-coverage considère qu’une classe ou un trait est couvert seulement quand toutes ses méthodes sont couvertes.

Couverture d’opcode

L’indicateur logiciel de couverture d’opcode mesure si chaque opcode d’une fonction ou d’une méthode a été exécuté lors de l’exécution de la suite de test. Une ligne de code se compile habituellement en plus d’un opcode. La couverture de ligne considère une ligne de code comme couverte dès que l’un de ses opcode est exécuté.

Couverture de branche

L’indicateur logiciel de couverture de branche mesure si l’expression booléenne de chaque structure de contrôle a été évaluée à true et à false pendant l’exécution de la suite de test.

Couverture de chemin

L’indicateur logiciel de couverture de chemin mesure si chacun des chemins d’exécution possible dans une fonction ou une méthode ont été suivis lors de l’exécution de la suite de test. Un chemin d’exécution est une séquence unique de branches depuis l’entrée de la fonction ou de la méthode jusqu’à sa sortie.

L’index Change Risk Anti-Patterns (CRAP)

L’index Change Risk Anti-Patterns (CRAP) est calculé en se basant sur la complexité cyclomatique et la couverture de code d’une portion de code. Du code qui n’est pas trop complexe et qui a une couverture de code adéquate aura un index CRAP faible. L’index CRAP peut être baissé en écrivant des tests et en refactorisant le code pour diminuer sa complexité.

Note

Les indicateurs logiciel de Couverture d’Opcode, Couverture de branche et de Couverture de chemin ne sont pas encore supportés par php-code-coverage.

Liste blanche de fichiers

Il est requis de configurer une liste blanche pour dire à PHPUnit quels fichiers de code source inclure dans le rapport de couverture de code. Cela peut être fait en utilisant l’option de ligne de commande --whitelist ou via le fichier de configuration (voir Inclure des fichiers de la couverture de code).

Les paramètres de configuration addUncoveredFilesFromWhitelist et processUncoveredFilesFromWhitelist sont disponibles pour configurer comment la liste blanche va se comporter:

  • addUncoveredFilesFromWhitelist="false" signifie que seuls les fichiers de la liste blanche qui ont au moins une ligne de code exécutée sont inclus dans le rapport de couverture
  • addUncoveredFilesFromWhitelist="true" (default) signifie que tous les fichiers de la liste blanche sont inclus dans le rapport de couverture même s’il n’y a pas une seule ligne de code d’exécutée
  • processUncoveredFilesFromWhitelist="false" (default) signifie qu’un fichier de la liste blanche qui n’a aucune ligne de code d’exécutée sera ajouté au rapport de couverture (si addUncoveredFilesFromWhitelist vaut true) mais ne sera pas chargé par PHPUnit et par conséquent pas analysé pour l’information correcte des lignes de code excécutables
  • processUncoveredFilesFromWhitelist="true" signifie qu’un fichier de la liste blanche qui n’a aucune ligne de code d’exécutée sera chargé par PHPUnit et pourra donc être analysé pour l’information correcte des lignes de code excécutables

Note

Notez que le chargement des fichiers de code source qui est effectué lorsque processUncoveredFilesFromWhitelist="true" est défini peut causer des problèmes quand un fichier de code source contient du code en dehors de la portée d’une classe ou d’une fonction, par exemple.

Ignorer des blocs de code

Parfois, vous avez des blocs de code que vous ne pouvez pas tester et que vous pouvez vouloir ignorer lors de l’analyse de la couverture de code. PHPUnit vous permet de le faire en utilisant les annotations @codeCoverageIgnore, @codeCoverageIgnoreStart et @codeCoverageIgnoreEnd comme montré dans Example 10.1.

Example 10.1 Utiliser les annotations @codeCoverageIgnore, @codeCoverageIgnoreStart et @codeCoverageIgnoreEnd
<?php
use PHPUnit\Framework\TestCase;

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

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

if (false) {
    // @codeCoverageIgnoreStart
    print '*';
    // @codeCoverageIgnoreEnd
}

exit; // @codeCoverageIgnore

Les lignes de code ignorées (marquées comme ignorées à l’aide des annotations) sont comptées comme exécutées (si elles sont exécutables) et ne seront pas mises en évidence.

Spécifier les méthodes couvertes

L’annotation @covers (voir 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 tester. Si elle est fournie, seules les informations de couverture de code pour la(les) méthode(s) indiquées seront prises en considération. Example 10.2 montre un exemple.

Example 10.2 Tests qui indiquent quelle(s) méthode(s) ils veulent couvrir
<?php
use PHPUnit\Framework\TestCase;

class BankAccountTest extends TestCase
{
    protected $ba;

    protected function setUp()
    {
        $this->ba = new BankAccount;
    }

    /**
     * @covers BankAccount::getBalance
     */
    public function testBalanceIsInitiallyZero()
    {
        $this->assertSame(0, $this->ba->getBalance());
    }

    /**
     * @covers BankAccount::withdrawMoney
     */
    public function testBalanceCannotBecomeNegative()
    {
        try {
            $this->ba->withdrawMoney(1);
        }

        catch (BankAccountException $e) {
            $this->assertSame(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers BankAccount::depositMoney
     */
    public function testBalanceCannotBecomeNegative2()
    {
        try {
            $this->ba->depositMoney(-1);
        }

        catch (BankAccountException $e) {
            $this->assertSame(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers BankAccount::getBalance
     * @covers BankAccount::depositMoney
     * @covers BankAccount::withdrawMoney
     */
    public function testDepositWithdrawMoney()
    {
        $this->assertSame(0, $this->ba->getBalance());
        $this->ba->depositMoney(1);
        $this->assertSame(1, $this->ba->getBalance());
        $this->ba->withdrawMoney(1);
        $this->assertSame(0, $this->ba->getBalance());
    }
}

Il est également possible d’indiquer qu’un test ne doit couvrir aucune méthode en utilisant l’annotation @coversNothing (voir @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.

Example 10.3 Un test qui indique qu’aucune méthode ne doit être couverte
<?php
use PHPUnit\DbUnit\TestCase

class GuestbookIntegrationTest extends TestCase
{
    /**
     * @coversNothing
     */
    public function testAddEntry()
    {
        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Hello world!");

        $queryTable = $this->getConnection()->createQueryTable(
            'guestbook', 'SELECT * FROM guestbook'
        );

        $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml")
                              ->getTable("guestbook");

        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}

Cas limites

Cette section présente des cas limites remarquables qui conduisent à des informations de couverture de code prêtant à confusion.

<?php
use PHPUnit\Framework\TestCase;

// Because it is "line based" and not statement base coverage
// one line will always have one coverage status
if (false) this_function_call_shows_up_as_covered();

// Due to how code coverage works internally these two lines are special.
// This line will show up as non executable
if (false)
    // This line will show up as covered because it is actually the
    // coverage of the if statement in the line above that gets shown here!
    will_also_show_up_as_covered();

// To avoid this it is necessary that braces are used
if (false) {
    this_call_will_never_show_up_as_covered();
}