Melhorias na cobertura do código PHP em 2020

Você sabia que suas métricas de cobertura de código mentem?



Em 2003, Derick Rethans lançou o Xdebug 1.2 . Pela primeira vez no ecossistema PHP , é possível coletar dados de cobertura de código. Em 2004, Sebastian Bergmann lançou o PHPUnit 2 , onde o usou pela primeira vez. Os desenvolvedores agora podem medir o desempenho de seus conjuntos de testes usando relatórios de cobertura.



Desde então, a funcionalidade foi movida para um componente de cobertura de código php independente e genérico . PHPDBG e PCOV surgiram como drivers alternativos . Mas, fundamentalmente, o processo principal para desenvolvedores não mudou nos últimos 16 anos.



Em agosto de 2020, com o lançamento do php-code-coverage 9.0 e seus lançamentos relacionados PHPUnit 9.3 e behat-code-verification 5.0 , uma nova maneira de estimar a cobertura tornou-se disponível.



Hoje vamos considerar



  1. Um rápido tour pelo básico
  2. Limitações
  3. Métricas alternativas
  4. Cobertura de filial
  5. Caminhos de cobertura
  6. Incluindo novas métricas
  7. Qual métrica usar?
  8. Há algum motivo para não incluir novas métricas?
  9. Resultado


Um rápido tour pelo básico



A maioria dos desenvolvedores de PHP está familiarizada com a ideia de teste de código automatizado. A ideia de cobertura de código está intimamente relacionada ao teste automatizado e é sobre medir a porcentagem de código que foi executado ou, como dizem, “coberto” por testes. Por exemplo, se você tiver o seguinte 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);
    }
}


Então, podemos escrever um teste PHPUnit conforme mostrado abaixo:



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


Depois de executar o teste, o PHPUnit confirma que atingimos 100% de cobertura neste exemplo trivial:







Limitações



No exemplo acima, entretanto, havia um pequeno erro potencial. Se $ denominador for 0 , obteremos uma divisão por erro zero. Vamos consertar isso e ver o que acontece:



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






Embora a linha 12 agora use a instrução if / else ternária (e ainda não escrevemos um teste para verificar se nosso tratamento de nulos está correto), o relatório nos diz que ainda temos 100% de cobertura de código.



Se parte da linha for coberta pelo teste, toda a linha será marcada como coberta . Isso pode ser enganoso!



Simplesmente calculando se uma linha é executada ou não, outras construções de código podem frequentemente ter os mesmos problemas, por exemplo:



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 da versão 2.3, o Xdebug foi capaz de coletar não apenas métricas familiares linha por linha, mas também métricas alternativas de cobertura de ramais e caminhos. A postagem do blog de Derik falando sobre esse recurso terminou com a declaração infame:

“Resta esperar até que Sebastian (ou outra pessoa) tenha tempo para atualizar o PHP_CodeCoverage para mostrar a cobertura de ramais e caminhos. Feliz hackeamento!

Derik Retans, janeiro de 2015 "


Após 5 anos de espera por este misterioso "alguém", decidi tentar implementar tudo sozinho. Muito obrigado a Sebastian Bergman por aceitar meu pedido de pull .



Cobertura de filial



Em todos, exceto no código mais simples, há lugares onde o caminho de execução pode divergir em dois ou mais caminhos. Isso acontece em todos os pontos de decisão, como if / else ou while . Cada lado desses pontos de divergência é um ramo separado. Se não houver ponto de decisão, o encadeamento de execução contém apenas um ramo.



Observe que, apesar de usar a metáfora da árvore, um branch neste contexto não é o mesmo que um branch de controle de versão, não os confunda!



Quando a cobertura de ramificação e caminho está habilitada, relatório HTML gerado com cobertura de código php, além do relatório de cobertura de linha regular, inclui add-ons para exibir cobertura de ramal e caminho. É assim que a cobertura de ramais se parece usando o mesmo exemplo de código de antes:







Como você pode ver, a caixa dinâmica na parte superior da página indica imediatamente que, embora tenhamos cobertura completa linha por linha, isso não se aplica à cobertura de ramais e caminhos ( caminhos são discutidos em detalhes na próxima seção).



Além disso, a linha 12 é destacada em amarelo para indicar que tem cobertura incompleta (uma linha com cobertura de 0% será exibida em vermelho, como de costume).



Finalmente, os mais atentos podem perceber que, ao contrário da cobertura linha a linha, mais linhas são destacadas em cores. Isso ocorre porque os ramos são calculados com base no fluxo de execução dentro do interpretador PHP. A primeira ramificação de cada função começa quando essa função é inserida. Isso contrasta com a cobertura baseada em string, em que apenas o corpo da função é considerado como contendo strings executáveis ​​e a própria declaração da função é considerada não executável.



Encontrar filiais



Essas diferenças entre o que o interpretador PHP considera ser um ramo de código logicamente separado e o modelo mental do desenvolvedor podem tornar as métricas difíceis de entender. Por exemplo, se você me perguntasse quantas ramificações existem em calculPercent () , eu responderia 2 (um caso especial para 0 e um caso geral). No entanto, olhando para o relatório de cobertura de código php acima, esta função de uma linha na verdade contém ... 4 ramos?!



Para entender o que o interpretador PHP significa, há um relatório de cobertura adicional no upstream. Ele mostra uma versão estendida da exibição de cada ramo, o que ajuda a identificar com mais eficiência o que está oculto no código-fonte. Se parece com isso:





A legenda diz: “Abaixo estão as linhas de código-fonte que representam cada branch de código que o Xdebug encontrou . Observe que uma ramificação não precisa ser igual a uma string: uma string pode conter várias ramificações e, portanto, aparecer mais de uma vez. Também tenha em mente que algumas ramificações podem estar implícitas, por exemplo, uma instrução if sempre tem um else no fluxo lógico, mesmo se você não o escreveu. "


Tudo isso ainda não é muito óbvio, mas você já pode entender quais ramificações estão realmente em calculPercent () :



  • O ramo 1 começa na entrada da função e inclui a verificação do denominador $ this->;
  • A execução é então dividida nos ramos 2 e 3 dependendo se o caso especial é tratado ou não;
  • A ramificação 4 é onde se fundem as ramificações 2 e 3. Consiste em retornar e sair da função.


A correspondência mental de branches para partes individuais do código-fonte é uma nova habilidade que requer um pouco de prática. Mas fazer isso com código de fácil leitura e compreensão é definitivamente mais fácil. Se o seu código estiver cheio de linhas simples que combinam várias partes da lógica, como em nosso exemplo, espere mais complexidade em comparação com o código onde tudo é estruturado e escrito em várias linhas, correspondendo completamente aos ramos. A mesma lógica escrita neste estilo seria assim:







Trevo



Se você exportar o relatório de cobertura de código php no formato Clover para transferi-lo para outro sistema, com a cobertura baseada em ramificação habilitada, os dados serão gravados nas chaves condicionais e cobertas . Anteriormente (ou se a cobertura de filial não estava habilitada), os valores exportados eram sempre zero.



Caminhos de cobertura



Os caminhos são combinações possíveis de ramos. O exemplo CalculePercent () tem dois caminhos possíveis, conforme mostrado acima:



  • Filial 1, Filial 2 e Filial 4;
  • Ramificação 1, ramificação 3 e ramificação 4.






No entanto, muitas vezes o número de caminhos é maior do que o número de ramificações, por exemplo, em código que contém muitas condicionais e loops. O exemplo a seguir, obtido da cobertura do código-php , tem 23 ramos, mas na verdade existem 65 caminhos diferentes para a função:



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


Se você não conseguir encontrar todos os 23 branches, lembre-se de que foreach pode aceitar um iterador vazio e se houver sempre um else invisível .


Sim, isso significa que 65 testes são necessários para 100% de cobertura.



O relatório HTML de cobertura de código php , como ramos, inclui uma visão adicional para cada caminho. Mostra quais estão cobertas com a massa e quais não estão.



PORCARIA



Habilitar a cobertura do caminho afeta ainda mais as métricas exibidas, ou seja, a pontuação CRAP . A definição publicada em crap4j.org usa a métrica de cobertura de caminho de porcentagem historicamente indisponível em PHP como entrada para o cálculo . Enquanto no PHP , a cobertura linha por linha sempre foi usada. Para pequenos recursos com boa cobertura, a pontuação CRAP provavelmente permanecerá a mesma ou até diminuirá. Mas para funções com muitos caminhos de execução e cobertura insuficiente, o valor aumentará significativamente.



Incluindo novas métricas



A cobertura de ramificação e caminho é ativada ou desativada em conjunto, uma vez que ambas são simplesmente representações diferentes dos mesmos dados de execução de código subjacente.



PHPUnit



Para PHPUnit 9.3+, métricas adicionais são desabilitadas por padrão e podem ser habilitadas tanto por meio da linha de comando quanto por meio do arquivo de configuração phpunit.xml , mas apenas quando executado em Xdebug . A tentativa de habilitar este recurso ao usar PCOV ou PHPDBG resultará em um aviso de incompatibilidade de configuração e a cobertura não será coletada.



  • No console, use a opção --path -acobertura : vendor / bin / phpunit --cobertura - caminho .
  • Em phpunit.xml, defina o atributo pathCoverage do elemento de cobertura como true .


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


No PHPUnit 9.3, o formato do arquivo de configuração foi seriamente alterado , então a estrutura acima provavelmente parece diferente da que você está acostumado.




cobertura-código-behat



Para behat-code-cover 5.0+, a configuração é feita em behat.yml , o atributo é chamado branchAndPathCoverage . Se você tentar habilitá-lo com um driver diferente do Xdebug , um aviso será emitido, mas a cobertura ainda será gerada. Isso facilita o uso do mesmo arquivo de configuração em ambientes diferentes. Se não for configurada explicitamente, a nova cobertura será habilitada por padrão ao executar no Xdebug .



Qual métrica usar?



Pessoalmente, eu ( Doug Wright ) usarei as novas métricas sempre que possível. Testei-os em vários códigos para ver o que é "normal". Em meus projetos, provavelmente, usarei uma abordagem híbrida, que mostrarei a seguir. Para projetos comerciais, a decisão de mudar para novas métricas, obviamente, deve ser feita por toda a equipe, e estou ansioso pela chance de comparar suas descobertas com as minhas.



Minha opinião



Cobertura 100% baseada em caminhos é, sem dúvida, o Santo Graal e, onde faz sentido aplicá-la, é uma boa métrica para buscar, mesmo que não o faça. Se você está escrevendo testes, ainda deve pensar em coisas como casos extremos. A cobertura baseada em caminho ajuda você a ter certeza de que está tudo bem.



No entanto, se um método contém dezenas, centenas ou mesmo milhares de caminhos (o que não é incomum para coisas bastante complexas), eu não perderia tempo escrevendo centenas de testes. É aconselhável parar às dez. O teste não é um fim em si mesmo, mas uma ferramenta de mitigação de risco e um investimento no futuro. Os testes devem valer a pena, e o tempo gasto nissoos testes dificilmente terão retorno. Em situações como essa, é melhor ter como objetivo uma boa cobertura de agências, pois isso pelo menos garante que você pense sobre o que está acontecendo em cada ponto de decisão.



Em casos de um grande número de caminhos (eles agora estão bem definidos com o CRAP honesto), eu avalio se o código em questão não faz muito e há uma maneira razoável de dividi-lo em funções menores (que já podem ser analisadas com mais detalhes)? Às vezes não, e tudo bem - não precisamos eliminar absolutamente todos os riscos do projeto. Até mesmo saber sobre eles é maravilhoso. Também é importante lembrar que os limites da função e seus testes de unidade isolados são uma separação artificial da lógica, não a verdadeira complexidade do seu software geral. Portanto, eu recomendaria não interromper funções grandes apenas por causa do número assustador de caminhos de execução. Faça isso apenas quando a separação reduzir a carga cognitiva e ajudar na percepção do código.



Há algum motivo para não incluir novas métricas?



Sim, desempenho. Não é nenhum segredo que o código Xdebug é incrivelmente lento em comparação com o desempenho normal do PHP . E se você ativar a cobertura de ramificações e caminhos, tudo será agravado pela adição de custos indiretos para todos os dados de execução adicionais que agora ele precisa rastrear.



A boa notícia é que ter que lidar com esses problemas inspirou o desenvolvedor a fazer melhorias gerais de desempenho dentro da cobertura do código php que beneficiará qualquer pessoa que use o Xdebug . O desempenho das suítes de teste varia muito, então é difícil julgar como isso afetará cada suíte de teste, mas coletar a cobertura baseada em string será mais rápido de qualquer maneira.



Ramos e caminhos ainda cobrem cerca de 3-5 vezes mais devagar. Isso deve ser levado em consideração. Considere habilitar seletivamente arquivos de teste individuais em vez de todo o conjunto de testes, ou um build noturno com "melhor cobertura" em vez de executar cada push.



O Xdebug 3 será significativamente mais rápido do que as versões atuais devido ao trabalho feito na modularização e desempenho, portanto, essas advertências devem ser vistas como específicas do Xdebug 2 apenas . Com a versão 3, mesmo considerando a sobrecarga de coleta de dados adicionais, é possível gerar cobertura baseada em filial e baseada em caminho em menos tempo do que leva hoje para obter cobertura linha por linha!





Testes conduzidos por Sebastian Bergmann, gráfico traçado por Derick Rethans




Resultado



Teste os novos recursos e escreva para nós. Eles são úteis? Idéias para visualização alternativa (possivelmente de outras linguagens) são especialmente interessantes.



Bem, estou sempre interessado em sua opinião sobre qual é o nível normal de cobertura de código.





No PHP Rússia em 29 de novembro, discutiremos todas as questões mais importantes sobre o desenvolvimento do PHP, sobre o que não está na documentação, mas o que dará ao seu código um novo nível.



Junte-se a nós na conferência: não apenas para ouvir reportagens e fazer perguntas aos melhores palestrantes do universo PHP, mas também para comunicação profissional (finalmente offline!) Em um ambiente acolhedor. Nossas comunidades: Telegram , Facebook , VKontakte , YouTube .



All Articles