Mejoras en la cobertura del código PHP en 2020

¿Sabías que las métricas de cobertura de tu código mienten?



En 2003, Derick Rethans lanzó Xdebug 1.2 . Por primera vez en el ecosistema PHP , es posible recopilar datos de cobertura de código. En 2004, Sebastian Bergmann lanzó PHPUnit 2 , donde lo utilizó por primera vez. Los desarrolladores ahora tienen la capacidad de medir el rendimiento de sus conjuntos de pruebas mediante informes de cobertura.



Desde entonces, la funcionalidad se ha trasladado a un componente de cobertura de código php genérico e independiente . PHPDBG y PCOV han aparecido como controladores alternativos . Pero fundamentalmente, el proceso central para los desarrolladores no ha cambiado en los últimos 16 años.



En agosto de 2020, con el lanzamiento de PHP-Code-Cobertura 9.0 y las versiones relacionadas PHPUnit 9.3 y Behat-Code-Cobertura 5.0 , se puso a disposición una nueva forma de estimar la cobertura .



Hoy consideraremos



  1. Un recorrido rápido por los conceptos básicos
  2. Limitaciones
  3. Métricas alternativas
  4. Cobertura de sucursales
  5. Cubriendo caminos
  6. Incluyendo nuevas métricas
  7. ¿Qué métrica usar?
  8. ¿Hay alguna razón para no incluir nuevas métricas?
  9. Salir


Un recorrido rápido por los conceptos básicos



La mayoría de los desarrolladores de PHP están familiarizados con la idea de las pruebas de código automatizadas. La idea de cobertura de código está estrechamente relacionada con las pruebas automatizadas y se trata de medir el porcentaje de código que se ha ejecutado o, como dicen, "cubierto" por las pruebas. Por ejemplo, si tiene el siguiente código:



<?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);
    }
}


Entonces podemos escribir una prueba PHPUnit como se muestra a continuación:



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


Después de ejecutar la prueba, PHPUnit confirma que hemos alcanzado el 100% de cobertura en este trivial ejemplo:







Limitaciones



En el ejemplo anterior, sin embargo, hubo un pequeño error potencial. Si $ denominador es 0 , obtenemos una división por error cero. Arreglemos eso y veamos qué sucede:



<?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;
    }
}






Aunque la línea 12 ahora usa la declaración ternaria if / else (y ni siquiera hemos escrito una prueba para verificar que nuestro manejo nulo sea correcto), el informe nos dice que todavía tenemos una cobertura de código del 100%.



Si parte de la línea está cubierta por la prueba, entonces toda la línea se marca como cubierta . ¡Esto puede ser engañoso!



Simplemente calculando si una línea se ejecuta o no, otras construcciones de código a menudo pueden tener los mismos problemas, por ejemplo:



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étricas alternativas



A partir de la versión 2.3, Xdebug pudo recopilar no solo métricas familiares línea por línea, sino también métricas alternativas de cobertura de ruta y rama. La publicación del blog de Derik que habla sobre esta característica terminó con la infame declaración:

“Queda por esperar hasta que Sebastian (o alguien más) tenga tiempo de actualizar PHP_CodeCoverage para mostrar la cobertura de la rama y la ruta. ¡Feliz piratería!

Derik Retans, enero de 2015 "


Después de 5 años de esperar a este misterioso "alguien más", decidí intentar implementarlo todo yo mismo. Muchas gracias a Sebastian Bergman por aceptar mi solicitud de extracción .



Cobertura de sucursales



En todo el código, excepto en el más simple, hay lugares donde la ruta de ejecución puede divergir en dos o más rutas. Esto sucede en cada punto de decisión, como cada si / si o mientras . Cada "lado" de estos puntos de divergencia es una rama separada. Si no hay un punto de decisión, el hilo de ejecución contiene solo una rama.



Tenga en cuenta que a pesar del uso de la metáfora del árbol, una rama en este contexto no es lo mismo que una rama de control de versiones, ¡no las confunda!



Cuando la cobertura de rama y ruta está habilitada, el informe HTML se genera con cobertura de código php, además del informe de cobertura de línea regular, incluye complementos para mostrar la cobertura de rama y ruta. Así es como se ve la cobertura de sucursales usando el mismo ejemplo de código que antes:







Como puede ver, el cuadro dinámico en la parte superior de la página indica inmediatamente que, aunque tenemos una cobertura completa línea por línea, esto no se aplica a la cobertura de sucursales y rutas ( las rutas se discuten en detalle en la siguiente sección).



Además, la línea 12 está resaltada en amarillo para indicar que tiene una cobertura incompleta (una línea con una cobertura del 0% se mostrará en rojo como de costumbre).



Finalmente, los más atentos pueden notar que, a diferencia de la cobertura línea por línea, más líneas están resaltadas en color. Esto se debe a que las ramas se calculan en función del flujo de ejecución dentro del intérprete de PHP. La primera rama de cada función comienza cuando se ingresa esa función. Esto contrasta con la cobertura basada en cadenas, donde solo se considera que el cuerpo de la función contiene cadenas ejecutables y la declaración de la función en sí misma se considera no ejecutable.



Encontrar ramas



Tales diferencias entre lo que el intérprete de PHP considera una rama de código lógicamente separada y el modelo mental del desarrollador pueden hacer que las métricas sean difíciles de entender. Por ejemplo, si me preguntara cuántas ramas hay en calculatePercent () , respondería que 2 (un caso especial para 0 y un caso general). Sin embargo, mirando el informe de cobertura de código php anterior, esta función de una línea en realidad contiene ... ¡¿4 ramas?!



Para comprender lo que significa el intérprete de PHP , hay un informe de cobertura adicional en el upstream. Muestra una versión extendida de la pantalla de cada rama, lo que ayuda a identificar de manera más eficiente lo que está oculto en el código fuente. Se parece a esto:





La leyenda dice: “A continuación se muestran las líneas de fuente que representan cada rama de código que encontró Xdebug . Tenga en cuenta que una rama no tiene que ser lo mismo que una cadena: una cadena puede contener varias ramas y, por lo tanto, aparecer más de una vez. También tenga en cuenta que algunas ramas pueden ser implícitas, por ejemplo, una instrucción if siempre tiene un else en el flujo lógico, incluso si no la escribió ".


Todo esto aún no es del todo obvio, pero ya puede comprender qué ramas están realmente en calculatePercent () :



  • La rama 1 comienza en la entrada de la función e incluye la verificación $ this-> denominator;
  • Luego, la ejecución se divide en las ramas 2 y 3 dependiendo de si se maneja o no el caso especial;
  • La rama 4 es donde se fusionan las ramas 2 y 3. Consiste en regresar y salir de la función.


Hacer coincidir mentalmente ramas con partes individuales del código fuente es una nueva habilidad que requiere un poco de práctica. Pero hacerlo con un código fácilmente legible y comprensible es definitivamente más fácil. Si su código está lleno de frases ingeniosas que combinan varias piezas de lógica, como en nuestro ejemplo, entonces espere más complejidad en comparación con el código donde todo está estructurado y escrito en varias líneas, correspondiendo completamente a las ramas. La misma lógica escrita en este estilo se vería así:







Trébol



Si exporta el informe de cobertura de código php en formato Clover para transferirlo a otro sistema, con la cobertura basada en sucursales habilitada, los datos se escribirán en las claves condicionales y condicionales cubiertas . Anteriormente (o si la cobertura de sucursales no estaba habilitada) los valores exportados siempre eran cero.



Cubriendo caminos



Los caminos son posibles combinaciones de ramas. El ejemplo calculatePercent () tiene dos rutas posibles, como se muestra arriba:



  • Rama 1, luego Rama 2, luego Rama 4;
  • Rama 1, luego rama 3 y luego rama 4.






Sin embargo, a menudo el número de rutas es mayor que el número de ramas, por ejemplo, en un código que contiene muchos condicionales y bucles. El siguiente ejemplo, tomado de php-code -cover , tiene 23 ramas, pero en realidad hay 65 rutas diferentes para la función:



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 no puede encontrar las 23 ramas, recuerde que foreach puede aceptar un iterador vacío, y si siempre hay un invisible else .


Sí, eso significa que se necesitan 65 pruebas para una cobertura del 100%.



El informe HTML de cobertura de código php , como las ramas, incluye una vista adicional para cada ruta. Muestra cuáles están cubiertos con la masa y cuáles no.



MIERDA



La habilitación de la cobertura de la ruta afecta aún más las métricas mostradas, es decir, la puntuación CRAP . La definición publicada en crap4j.org utiliza la métrica de cobertura de ruta porcentual históricamente no disponible en PHP como entrada para el cálculo . Mientras que en PHP , siempre se ha utilizado la cobertura línea por línea. Para funciones pequeñas con buena cobertura, es probable que la puntuación CRAP permanezca igual o incluso disminuya. Pero para funciones con muchas rutas de ejecución y cobertura deficiente, el valor aumentará significativamente.



Incluyendo nuevas métricas



La cobertura de rama y ruta se habilita o deshabilita a la vez, ya que ambas son simplemente representaciones diferentes de los mismos datos de ejecución de código subyacente.



PHPUnit



Para PHPUnit 9.3+, las métricas adicionales están deshabilitadas de forma predeterminada y se pueden habilitar a través de la línea de comandos o mediante el archivo de configuración phpunit.xml , pero solo cuando se ejecuta en Xdebug . Si intenta habilitar esta función mientras usa PCOV o PHPDBG, se generará una advertencia de incompatibilidad de configuración y no se recopilará la cobertura.



  • En la consola, use la opción --path -cover : vendor / bin / phpunit - path-coberturas .
  • En phpunit.xml, establecer la cobertura del elemento pathCoverage atributo de verdad .


<?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>


En PHPUnit 9.3, el formato del archivo de configuración se ha cambiado seriamente , por lo que la estructura anterior probablemente se ve diferente a la que está acostumbrado.




cobertura de código behat



Para behat-code-cover 5.0+, la configuración se realiza en behat.yml , el atributo se llama branchAndPathCoverage . Si intenta habilitarlo con un controlador que no sea Xdebug , se emitirá una advertencia, pero se seguirá generando cobertura. Esto es para facilitar el uso del mismo archivo de configuración en diferentes entornos. Si no se configura explícitamente, la nueva cobertura se habilitará de forma predeterminada cuando se ejecute en Xdebug .



¿Qué métrica usar?



Personalmente, yo ( Doug Wright ) utilizaré las nuevas métricas siempre que sea posible. Los probé en varios códigos para ver qué es "normal". En mis proyectos, lo más probable es que utilice un enfoque híbrido, que mostraré a continuación. Para los proyectos comerciales, la decisión de cambiar a nuevas métricas, obviamente, debe ser tomada por todo el equipo, y espero tener la oportunidad de comparar sus hallazgos con los míos.



Mi opinión



La cobertura 100% basada en rutas es sin duda el santo grial, y donde tiene sentido aplicarlo es una buena métrica por la que luchar, incluso si no lo hace. Si escribe pruebas, debe pensar en cosas como casos extremos. La cobertura basada en rutas le ayuda a asegurarse de que esté bien.



Sin embargo, si un método contiene decenas, cientos o incluso miles de rutas (que en realidad no es infrecuente para cosas bastante complejas), no perdería el tiempo escribiendo cientos de pruebas. Es aconsejable detenerse a las diez. Las pruebas no son un fin en sí mismas, sino una herramienta de mitigación de riesgos y una inversión en el futuro. Las pruebas deberían dar sus frutos, y el tiempo invertido en tantoEs poco probable que las pruebas valgan la pena. En situaciones como esta, es mejor apuntar a una buena cobertura de sucursales, ya que al menos asegura que usted piense en lo que está sucediendo en cada punto de decisión.



En los casos de una gran cantidad de rutas (ahora están bien definidas con CRAP honesto), evalúo si el código en cuestión no hace demasiado, y ¿hay una forma razonable de dividirlo en funciones más pequeñas (que ya se pueden analizar con más detalle)? A veces no, y eso está bien, no necesitamos eliminar absolutamente todos los riesgos del proyecto. Incluso conocerlos es maravilloso. También es importante recordar que los límites de las funciones y sus pruebas unitarias aisladas son una separación artificial de la lógica, no la verdadera complejidad de su software en general. Por lo tanto, recomendaría no romper funciones grandes solo por la enorme cantidad de rutas de ejecución. Haga esto solo cuando la separación reduzca la carga cognitiva y ayude a la percepción del código.



¿Hay alguna razón para no incluir nuevas métricas?



Sí, rendimiento. No es ningún secreto que el código Xdebug es increíblemente lento en comparación con el rendimiento normal de PHP . Y si activa la cobertura de ramas y rutas, todo se agrava con la adición de gastos generales para todos los datos de ejecución adicionales que ahora necesita rastrear.



La buena noticia es que tener que abordar estos problemas ha inspirado al desarrollador a realizar mejoras generales de rendimiento dentro de la cobertura de código php que beneficiarán a cualquiera que use Xdebug . El rendimiento de los conjuntos de pruebas varía mucho, por lo que es difícil juzgar cómo afectará esto a cada conjunto de pruebas, pero la recopilación de cobertura basada en cadenas será más rápida de todos modos.



Todavía es unas 3-5 veces más lento crear cobertura a partir de ramas y caminos. Esto debe tenerse en cuenta. Considere habilitar selectivamente archivos de prueba individuales en lugar de todo el conjunto de pruebas, o una compilación nocturna con "mejor cobertura" en lugar de ejecutar cada impulso.



Xdebug 3 será significativamente más rápido que las versiones actuales debido al trabajo realizado en la modularización y el rendimiento, por lo que estas advertencias deben considerarse específicas de Xdebug 2 únicamente . Con la versión 3, incluso considerando la sobrecarga de recopilar datos adicionales, es posible generar cobertura basada en sucursales y en ruta en menos tiempo del que se necesita hoy para obtener cobertura línea por línea.





Pruebas realizadas por Sebastian Bergmann, gráfico trazado por Derick Rethans




Salir



Pruebe las nuevas funciones y escríbanos. ¿Son útiles? Las ideas para la visualización alternativa (posiblemente de otros lenguajes) son especialmente interesantes.



Bueno, siempre estoy interesado en tu opinión sobre cuál es el nivel normal de cobertura de código.





En PHP Rusia el 29 de noviembre, discutiremos todas las preguntas más importantes sobre el desarrollo de PHP, sobre lo que no está en la documentación, pero qué le dará a su código un nuevo nivel.



Únase a nosotros en la conferencia: no solo para escuchar informes y hacer preguntas a los mejores oradores del universo PHP, sino también para la comunicación profesional (¡finalmente fuera de línea!) En un ambiente cálido. Nuestras comunidades: Telegram , Facebook , VKontakte , YouTube .



All Articles