Améliorations de la couverture du code PHP en 2020

Saviez-vous que vos métriques de couverture de code mentent?



En 2003, Derick Rethans a publié Xdebug 1.2 . Pour la première fois dans l'écosystème PHP , il est possible de collecter des données de couverture de code. En 2004, Sebastian Bergmann a publié PHPUnit 2 , où il l'a utilisé pour la première fois. Les développeurs ont désormais la possibilité de mesurer les performances de leurs suites de tests à l'aide de rapports de couverture.



Depuis lors, la fonctionnalité a été déplacée vers un composant générique et indépendant de couverture de code php . PHPDBG et PCOV sont apparus comme des pilotes alternatifs . Mais fondamentalement, le processus de base pour les développeurs n'a pas changé au cours des 16 dernières années.



En août 2020, avec la sortie de php-code-coverage 9.0 et de ses versions associées PHPUnit 9.3 et behat-code-coverage 5.0 , une nouvelle façon d'estimer la couverture est devenue disponible.



Aujourd'hui, nous allons considérer



  1. Un rapide tour d'horizon des bases
  2. Limites
  3. Métriques alternatives
  4. Couverture des succursales
  5. Couvrant des chemins
  6. Y compris de nouvelles métriques
  7. Quelle métrique utiliser?
  8. Y a-t-il des raisons de ne pas inclure de nouvelles mesures?
  9. Résultat


Un rapide tour d'horizon des bases



La plupart des développeurs PHP connaissent l'idée du test de code automatisé. L'idée de couverture de code est étroitement liée aux tests automatisés et consiste à mesurer le pourcentage de code qui a été exécuté ou, comme on dit, «couvert» par les tests. Par exemple, si vous avez le code suivant:



<?php
class PercentCalculator
{
    public function __construct(int $numerator, int $denominator)
    {
        $this->numerator = $numerator;
        $this->denominator = $denominator;
    }

    public function calculatePercent(): float
    {
        return round($this->numerator / $this->denominator * 100, 1);
    }
}


Ensuite, nous pouvons écrire un test PHPUnit comme indiqué ci-dessous:



<?php
class PercentCalculatorTest extends PHPUnit\Framework\TestCase
{
    public function testTwentyIntoForty(): void
    {
        $calculator = new PercentCalculator(20, 40);
        self::assertEquals(50.0, $calculator->calculatePercent());
    }
}


Après avoir exécuté le test, PHPUnit confirme que nous avons atteint une couverture de 100% dans cet exemple trivial:







Limites



Dans l'exemple ci-dessus, cependant, il y avait une petite erreur potentielle. Si $ dénominateur est 0 , alors nous obtenons une division par zéro erreur. Corrigeons cela et voyons ce qui se passe:



<?php
class PercentCalculator
{
    public function __construct(int $numerator, int $denominator)
    {
        $this->numerator = $numerator;
        $this->denominator = $denominator;
    }

    public function calculatePercent(): float
    {
        //     ,
        //     
        //   
        return $this->denominator ? round($this->numerator / $this->denominator * 100, 1) : 0.0;
    }
}






Même si la ligne 12 utilise maintenant l' instruction ternaire if / else (et nous n'avons même pas écrit de test pour vérifier que notre gestion des null est correcte), le rapport nous indique que nous avons toujours une couverture de code à 100%.



Si une partie de la ligne est couverte par le test, alors la ligne entière est marquée comme couverte . Cela peut être trompeur!



En calculant simplement si une ligne est exécutée ou non, d'autres constructions de code peuvent souvent avoir les mêmes problèmes, par exemple:



if ($a || $b || $c) { //  ** 
    doSomething();    //     100% 
}

public function pluralise(string $thing, int $count): string
{
    $string = $count . ' ' . $thing;

    if ($count > 1) {   //     $count >= 2,  - 100%
        $string .= 's'; //      $count === 1,
    }                   //      , 

    return $string;
}


Métriques alternatives



Depuis la version 2.3, Xdebug a pu collecter non seulement des métriques ligne par ligne familières, mais également des métriques alternatives de couverture de branche et de chemin. Le billet de blog de Derik parlant de cette fonctionnalité s'est terminé par la tristement célèbre déclaration:

«Il reste à attendre que Sebastian (ou quelqu'un d'autre) ait le temps de mettre à jour PHP_CodeCoverage pour afficher la couverture des branches et des chemins. Bon piratage!

Derik Retans, janvier 2015 "


Après 5 ans d'attente pour ce mystérieux «quelqu'un d'autre», j'ai décidé d'essayer de tout mettre en œuvre moi-même. Un grand merci à Sebastian Bergman pour avoir accepté ma demande de tirage .



Couverture des succursales



Dans tous les codes sauf le plus simple, il y a des endroits où le chemin d'exécution peut diverger en deux chemins ou plus. Cela se produit à chaque point de décision, comme à chaque if / else ou while . Chaque «côté» de ces points de divergence est une branche distincte. S'il n'y a pas de point de décision, le flux ne contient qu'une seule branche.



Notez que malgré l'utilisation de la métaphore de l'arbre, une branche dans ce contexte n'est pas la même qu'une branche de contrôle de version, ne confondez pas les deux!



Lorsque la couverture des branches et des chemins est activée, rapport HTML généré avec php-code-coverage, en plus du rapport de couverture de ligne standard, inclut des modules complémentaires pour afficher la couverture de branche et de chemin. Voici à quoi ressemble la couverture de branche en utilisant le même exemple de code que précédemment:







comme vous pouvez le voir, le tableau croisé dynamique en haut de la page indique immédiatement que, bien que nous ayons une couverture ligne par ligne complète, cela ne s'applique pas à la couverture de branche et de chemin ( les chemins sont décrits en détail dans la section suivante).



De plus, la ligne 12 est surlignée en jaune pour indiquer qu'elle a une couverture incomplète (une ligne avec une couverture de 0% sera affichée en rouge comme d'habitude).



Enfin, les plus attentifs remarqueront que, contrairement à la couverture ligne par ligne, plus de lignes sont mises en évidence en couleur. En effet, les branches sont calculées en fonction du flux d'exécution dans l'interpréteur PHP. La première branche de chaque fonction démarre lorsque cette fonction est entrée. Cela contraste avec la couverture basée sur des chaînes, où seul le corps de la fonction est considéré comme contenant des chaînes exécutables, et la déclaration de fonction elle-même est considérée comme non exécutable.



Trouver des succursales



De telles différences entre ce que l'interpréteur PHP considère comme une branche de code logiquement séparée et le modèle mental du développeur peuvent rendre les métriques difficiles à comprendre. Par exemple, si vous me demandiez combien de branches sont dans CalculatePercent () , je répondrais à 2 (un cas particulier pour 0 et un cas général). Cependant, en regardant le rapport de couverture de code php ci- dessus, cette fonction sur une ligne contient en fait ... 4 branches?!



Pour comprendre ce que signifie l' interpréteur PHP , il existe un rapport de couverture supplémentaire en amont. Il montre une version étendue de l'affichage de chaque branche, ce qui aide à identifier plus efficacement caché dans le code source. Cela ressemble à ceci:





La légende se lit comme suit: «Voici les lignes de source qui représentent chaque branche de code trouvée par Xdebug . Notez qu'une branche n'a pas besoin d'être identique à une chaîne: une chaîne peut contenir plusieurs branches et donc apparaître plus d'une fois. Gardez également à l'esprit que certaines branches peuvent être implicites, par exemple, une instruction if a toujours un else dans le flux logique, même si vous ne l'avez pas écrit. "


Tout cela n'est pas encore tout à fait évident, mais vous pouvez déjà comprendre quelles branches sont réellement dans CalculatePercent () :



  • La branche 1 commence à l'entrée de la fonction et inclut la vérification du dénominateur $ this->;
  • L'exécution est ensuite scindée en branches 2 et 3 selon que le cas particulier est traité ou non;
  • La branche 4 est le point de fusion des branches 2 et 3. Elle consiste à retourner et à quitter la fonction.


Faire correspondre mentalement des branches à des parties individuelles du code source est une nouvelle compétence qui demande un peu de pratique. Mais le faire avec un code facilement lisible et compréhensible est certainement plus facile. Si votre code est plein de one-liners intelligents qui combinent plusieurs éléments de logique, comme dans notre exemple, alors attendez-vous à plus de complexité par rapport au code où tout est structuré et écrit sur plusieurs lignes, correspondant complètement aux branches. La même logique écrite dans ce style ressemblerait à ceci:







Trèfle



Si vous exportez le code de php-couverture- rapport dans Clover forme pour le transférer à un autre système, avec une couverture par agence activée, les données seront écrites dans les conditionals et les clés de coveredconditionals . Auparavant (ou si la couverture de branche n'était pas activée), les valeurs exportées étaient toujours nulles.



Couvrant des chemins



Les chemins sont des combinaisons possibles de branches. L'exemple CalculatePercent () a deux chemins possibles, comme indiqué ci-dessus:



  • Branche 1, puis Branche 2, puis Branche 4;
  • Branche 1, puis branche 3, puis branche 4.






Cependant, souvent le nombre de chemins est supérieur au nombre de branches, par exemple, dans un code qui contient de nombreuses conditions et boucles. L'exemple suivant, tiré de php-code-coverage , a 23 branches, mais il existe en fait 65 chemins différents pour la fonction:



final class File extends AbstractNode
{
    public function numberOfTestedMethods(): int
    {
        if ($this->numTestedMethods === null) {
            $this->numTestedMethods = 0;

            foreach ($this->classes as $class) {
                foreach ($class['methods'] as $method) {
                    if ($method['executableLines'] > 0 &&
                        $method['coverage'] === 100) {
                        $this->numTestedMethods++;
                    }
                }
            }

            foreach ($this->traits as $trait) {
                foreach ($trait['methods'] as $method) {
                    if ($method['executableLines'] > 0 &&
                        $method['coverage'] === 100) {
                        $this->numTestedMethods++;
                    }
                }
            }
        }

        return $this->numTestedMethods;
    }
}


Si vous ne trouvez pas les 23 branches, rappelez-vous que foreach peut accepter un itérateur vide, et s'il y a toujours un autre invisible .


Oui, cela signifie que 65 tests sont nécessaires pour une couverture à 100%.



Le rapport HTML de couverture de code php , comme les branches, comprend une vue supplémentaire pour chaque chemin. Il montre ceux qui sont recouverts de pâte et ceux qui ne le sont pas.



MERDE



L'activation de la couverture de chemin affecte en outre les métriques affichées, à savoir le score CRAP . La définition publiée sur crap4j.org utilise la métrique de couverture de chemin en pourcentage historiquement indisponible en PHP comme entrée pour le calcul . Alors qu'en PHP , la couverture ligne par ligne a toujours été utilisée. Pour les petits éléments avec une bonne couverture, le score CRAP est susceptible de rester le même ou même de diminuer. Mais pour les fonctions avec de nombreux chemins d'exécution et une faible couverture, la valeur augmentera considérablement.



Y compris de nouvelles métriques



La couverture de branche et de chemin est activée ou désactivée ensemble, car les deux sont simplement des représentations différentes des mêmes données d'exécution de code sous-jacentes.



PHPUnit



Pour PHPUnit 9.3+, les métriques supplémentaires sont désactivées par défaut et peuvent être activées via la ligne de commande ou via le fichier de configuration phpunit.xml , mais uniquement lors de l'exécution sous Xdebug . Tenter d'activer cette fonctionnalité lors de l'utilisation de PCOV ou PHPDBG entraînera un avertissement d'incompatibilité de configuration et la couverture ne sera pas collectée.



  • Dans la console, utilisez l'option --path-coverage : vendor / bin / phpunit - path-coverage .
  • En phpunit.xml, définir la couverture de l' élément pathCoverage attribut à vrai .


<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
    <testsuites>
        <testsuite name="default">
            <directory>tests</directory>
        </testsuite>
    </testsuites>

    <coverage pathCoverage="true" processUncoveredFiles="true" cacheDirectory="build/phpunit/cache">
        <include>
            <directory suffix=".php">src</directory>
        </include>

        <report>
            <text outputFile="php://stdout"/>
            <html outputDirectory="build/coverage"/>
        </report>

    </coverage>
</phpunit>


Dans PHPUnit 9.3, le format du fichier de configuration a été sérieusement modifié , donc la structure ci-dessus est probablement différente de celle à laquelle vous êtes habitué.




couverture-code-behat



Pour behat-code-cover 5.0+, le réglage est effectué dans behat.yml , l'attribut est appelé branchAndPathCoverage . Si vous essayez de l'activer avec un pilote autre que Xdebug , un avertissement sera émis, mais la couverture sera toujours générée. Ceci permet de faciliter l'utilisation du même fichier de configuration dans différents environnements. Si elle n'est pas explicitement configurée, la nouvelle couverture sera activée par défaut lors de l'exécution sous Xdebug .



Quelle métrique utiliser?



Personnellement, je ( Doug Wright ) utiliserai les nouvelles métriques autant que possible. Je les ai testés sur différents codes pour voir quelle est la «norme». Sur mes projets, très probablement, j'utiliserai une approche hybride, que je montrerai ci-dessous. Pour les projets commerciaux, la décision de passer à de nouvelles métriques doit évidemment être prise par toute l'équipe, et j'ai hâte de pouvoir comparer leurs résultats avec les miens.



Mon avis



Une couverture à 100% basée sur les chemins est sans aucun doute le Saint Graal, et là où il est logique de l'appliquer, c'est une bonne mesure à atteindre, même si vous ne le faites pas. Si vous écrivez des tests, vous devriez toujours penser à des choses comme les cas extrêmes. La couverture basée sur le chemin vous aide à vous assurer que tout va bien.



Cependant, si une méthode contient des dizaines, des centaines, voire des milliers de chemins (ce qui n'est en fait pas rare pour des choses assez complexes), je ne perdrais pas de temps à écrire des centaines de tests. Il est sage de s'arrêter à dix heures. Les tests ne sont pas une fin en soi, mais un outil d'atténuation des risques et un investissement dans le futur. Les tests devraient porter leurs fruits, et le temps consacré à celail est peu probable que les tests portent leurs fruits. Dans de telles situations, il est préférable de viser une bonne couverture des succursales, car cela garantit au moins que vous réfléchissez à ce qui se passe à chaque point de décision.



Dans le cas d'un grand nombre de chemins (ils sont maintenant bien définis avec un CRAP honnête), j'évalue si le code en question n'en fait pas trop, et existe-t-il un moyen raisonnable de le décomposer en fonctions plus petites (qui peuvent déjà être analysées plus en détail)? Parfois non, et c'est normal - nous n'avons pas besoin d'éliminer absolument tous les risques du projet. Même les connaître est merveilleux. Il est également important de se rappeler que les limites des fonctions et leurs tests unitaires isolés sont une séparation artificielle de la logique et non la véritable complexité de votre logiciel global. Par conséquent, je recommanderais de ne pas interrompre les fonctions volumineuses simplement à cause du nombre impressionnant de chemins d'exécution. Ne faites cela que là où la séparation réduit la charge cognitive et aide à la perception du code.



Y a-t-il des raisons de ne pas inclure de nouvelles mesures?



Oui, la performance. Ce n'est un secret pour personne que le code Xdebug est incroyablement lent par rapport aux performances PHP normales . Et si vous activez la couverture des branches et des chemins, alors tout est aggravé par l'ajout de frais généraux pour toutes les données d'exécution supplémentaires dont il a maintenant besoin pour suivre.



La bonne nouvelle est que le fait de s'attaquer à ces problèmes a inspiré le développeur à apporter des améliorations générales des performances au sein de php-code-coverage qui profiteront à toute personne utilisant Xdebug . Les performances des suites de tests varient considérablement, il est donc difficile de juger comment cela affectera chaque suite de tests, mais la collecte de la couverture basée sur des chaînes sera de toute façon plus rapide.



Il est encore environ 3 à 5 fois plus lent pour créer une couverture à partir des branches et des chemins. Cela doit être pris en compte. Envisagez d'activer sélectivement les fichiers de test individuels plutôt que la suite de tests entière, ou une compilation nocturne avec une «meilleure couverture» au lieu d'exécuter chaque push.



Xdebug 3 sera beaucoup plus rapide que les versions actuelles en raison du travail effectué sur la modularisation et les performances, donc ces mises en garde doivent être considérées comme spécifiques à Xdebug 2 uniquement . Avec la version 3, même en tenant compte de la surcharge de collecte de données supplémentaires, il est possible de générer une couverture basée sur les branches et sur les chemins en moins de temps qu'il n'en faut aujourd'hui pour obtenir une couverture ligne par ligne!





Tests réalisés par Sebastian Bergmann, graphique tracé par Derick Rethans




Résultat



Veuillez tester les nouvelles fonctionnalités et nous écrire. Sont-ils utiles? Les idées de visualisation alternative (éventuellement d'autres langues) sont particulièrement intéressantes.



Eh bien, je suis toujours intéressé par votre opinion sur le niveau normal de couverture du code.





À PHP Russie le 29 novembre, nous discuterons de toutes les questions les plus importantes sur le développement PHP, sur ce qui n'est pas dans la documentation, mais sur ce qui donnera à votre code un nouveau niveau.



Rejoignez-nous à la conférence: non seulement pour écouter des reportages et poser des questions aux meilleurs intervenants de l'univers PHP, mais aussi pour une communication professionnelle (enfin hors ligne!) Dans une ambiance chaleureuse. Nos communautés: Telegram , Facebook , VKontakte , YouTube .



All Articles