terça-feira, 29 de janeiro de 2019

Code Coverage com C#, OpenCover, ReportGenerator e Cake

por   em 15/10/2017 no site Lambda3


Resultado de imagem para code coverage c#
MSDN Blogs - Microsoft

Se você trabalha com Visual Studio, deve saber que hoje somente a versão Enterprise fornece a funcionalidade de code coverage. Eu não acho que ter o code coverage do projeto deveria ser algo “premium”, então confiei que existiriam ferramentas grátis para essa tarefa. Neste post vou mostrar como fazer isso utilizando ferramentas todas feitas em C# com o Cake.
Recentemente comecei em um projeto que já tinha código escrito, prestes a ir para produção, porém, sem nenhum teste escrito. Quando entrei, uma das minhas funções foi adaptar o código para que fosse possível escrever testes de unidade, e trabalhar para aumentar o code coverage (cobertura de código) do projeto. Então pensei: preciso ser capaz de dizer qual é o code coverage desse projeto para entender quais partes continuam sem testes.
Desfrutei de umas das vantagens de trabalhar na Lambda3, perguntando para as pessoas que estão trabalhando com .NET quais ferramentas elas usavam. Foi aí que o Chico me recomendou uma ferramenta open source chamada OpenCover, que ele estava usando no projeto em que estava atuando.
O OpenCover faz exatamente o que eu precisava, mas eu estava na vibe de automatizar tudo com o Cake, e claramente repetir essa tarefa era digna de automatização. Então decidi fazer isso utilizando um script cake.
Para esse post vou usar um projeto de exemplo em .NET bem simples com testes escritos em NUnit 3.

Cake

Se você não faz ideia do que é o Cake, leia antes esse post que eu fiz recentemente sobre o assunto. Também é preciso fazer o setup inicial do Cake, que é muito simples e está descrito no primeiro post. Você também pode vê-lo na documentação aqui.
O Cake está em um momento de transição para o .NET Core na sua versão 0.22, e ela vai vir com várias breaking changes. Eu recomendo você pinar a versão do Cake para 0.21.1, conforme descrito aqui, caso queira seguir esse post sem problemas. Pelo menos até a transição estar completa.
Como disse no primeiro post, o Cake já vem com muitos aliases embutidos para tarefas comuns, e ele já tinha aliases pra todas tasks que eu usei aqui. Então eu só precisei entender como elas funcionavam e escrever as tasks.
O primeiro passo pra configurar um script Cake capaz de gerar um code coverage report é garantir que o código esteja sempre atualizado, ou seja, o projeto precisa ser recompilado antes que a task de code coverage seja executada. Por isso comecei com a tarefa de MSBuild.
1
2
3
4
5
6
7
8
9
10
Task("BuildTest")
    .Does(() =>
    {
        MSBuild("./Calculadora.Tests/Calculadora.Tests.csproj",
            new MSBuildSettings {
                Verbosity = Verbosity.Minimal,
                Configuration = "Debug"
            }
        );
    });
Eu apontei qual era o arquivo csproj e fiz o mínimo de configuração que precisava, com essa task a DLL do projeto estará sempre atualizada antes de gerar um novo report.

Code Coverage com OpenCover

OpenCover é uma ferramenta open source excelente feita em C# que gera o code coverage de projetos .NET. Na verdade ela faz mais que isso, ela também gera dados de branch coverage, complexidade ciclomáticacomplexidade NPath e, em versões futurasCRAP score, que te ajudam a entender quais códigos precisam ser refatorados.
O OpenCover tem algumas limitações: ele só roda no Windows, e até por consequência disso, ainda não tem um port stable para .NET Core. Para pessoas que utilizam Mac ou Linux, eu ainda não achei uma boa alternativa (se conhecer, compartilhe nos comentários).
O OpenCover pode ser baixado por NuGet e é basicamente uma aplicação de linha de comando, ela possui vários parâmetros que permitem a configuração do projeto. Por exemplo: excluir classes que não devem ser consideradas, incluir módulos, filtrar por nomes, etc. Você pode ver todos os parâmetros disponíveis na documentação do projeto.
Escolhido o OpenCover, o segundo passo então seria rodar os testes de fato. Mas ele já faz isso internamente, gerando os dados necessários para exibir o code coverage do código junto com a execução dos testes.
Aqui está a documentação para usar o OpenCover com o Cake. Para o meu projeto, essa task ficou assim:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#tool "nuget:?package=OpenCover"
#tool "nuget:?package=NUnit.ConsoleRunner"
 
Task("OpenCover")
    .IsDependentOn("BuildTest")
    .Does(() =>
    {
        var openCoverSettings = new OpenCoverSettings()
        {
            Register = "user",
            SkipAutoProps = true,
            ArgumentCustomization = args => args.Append("-coverbytest:*.Tests.dll").Append("-mergebyhash")
        };
 
        var outputFile = new FilePath("./GeneratedReports/CalculadoraReport.xml");
 
        OpenCover(tool => {
                var testAssemblies = GetFiles("./Calculadora.Tests/bin/Debug/Calculadora.Tests.dll");
                tool.NUnit3(testAssemblies);
            },
            outputFile,
            openCoverSettings
                .WithFilter("+[Calculadora*]*")
                .WithFilter("-[Calculadora.Tests]*")
        );
    });
Repare que um dos parâmetros é um FilePath, certifique-se que essa pasta está criada e você tem as permissões necessárias para que o script a acesse. Do contrário você receberá um erro.
No script acima dá pra observar alguns detalhes do Cake. Primeiro, como o OpenCover é distribuído como pacote NuGet, eu preciso adicionar uma referência à essa ferramenta com o #tool. Essa linha faz o Cake baixar o pacote NuGet e adicioná-lo na pasta de tools, para que ele possa usá-lo. Da mesma forma, eu indico que meu script precisa de NUnit para rodar.
Também dá pra ver que é possível tornar uma task dependente da outra, eu posso simplesmente rodar a task OpenCover e dizer que ela depende da task BuildTest. O Cake se encarrega de rodar elas na ordem certa.
A outra boa sacada do Cake presente no script acima, é que se o alias usado não expõe algum parâmetro da ferramenta que você precisa usar, é sempre possível passar argumentos extras com o campo ArgumentCustomization. Assim você não fica preso a um alias desatualizado.
Ao final da execução dessa task será gerado um XML com todos os dados necessários na pasta indicada na configuração da task. Mas como eu leio esse monte de XML pra entender o relatório?

Resultados legíveis com ReportGenerator

É aí que entra o ReportGenerator, outro projeto open source que tem o papel de interpretar arquivos de code coverage e exibir de uma maneira muito fácil de se analisar.
Ele não entende apenas OpenCover, o ReportGenerator é capaz de gerar dados a partir de várias outras ferramentas de code coverage. Ele também consegue gerar um histórico de evolução do código, tanto individual de cada classe quanto do projeto inteiro.
Essa é a documentação do alias do ReportGenerator para o Cake. Meu script ficou assim:
1
2
3
4
5
6
7
8
9
10
11
12
13
#tool "nuget:?package=ReportGenerator"
 
Task("ReportGenerator")
    .IsDependentOn("OpenCover")
    .Does(() =>
    {
        var reportGeneratorSettings = new ReportGeneratorSettings()
        {
            HistoryDirectory = new DirectoryPath("./GeneratedReports/ReportsHistory")
        };
 
        ReportGenerator("./GeneratedReports/CalculadoraReport.xml", "./GeneratedReports/ReportGeneratorOutput", reportGeneratorSettings);
    });
Rodando essa task agora, consigo ver o resultado de cada classe, bem detalhado:
code coverage report
As linhas vermelhas indicam que nenhum teste passou por elas, as amarelas indicam que alguns testes passaram por elas, mas não cobriram todos os caminhos possíveis (ex.: os dois fluxos de um if), e as verdes indicam que todos os cenários daquela linha foram cobertos por testes.
Por último, eu quis trocar a task Default do Cake, para que quando eu chame o build.ps1 ele automaticamente execute minha task ReportGenerator e abra o resultado HTML no meu browser, pra isso eu criei essa task:
1
2
3
4
5
6
7
8
9
10
11
Task("Default")
    .IsDependentOn("ReportGenerator")
    .Does(() =>
    {
        if (IsRunningOnWindows())
        {
            var reportFilePath = ".\\GeneratedReports\\ReportGeneratorOutput\\index.htm";
          
            StartProcess("explorer", reportFilePath);
        }
    });

Conclusão

Agora o projeto tem code coverage e histórico, tudo com ferramentas open source. Com isso é possível enxergar os pontos mais críticos que precisam de testes, entender a complexidade do código escrito com as métricas disponíveis e acompanhar a evolução do projeto.
Essas ferramentas facilitam muito a vida de quem desenvolve software, e é ótimo ver que a comunidade .NET está criando ferramentas grátis e open source para resolver esses problemas. Utilizar o Cake facilita ainda mais o uso e a integração delas, pra que você não precise perder muito tempo com isso e possa focar no que importa.
O projeto de exemplo que usei com o script completo estão no meu repositório no GitHub, sob licença MIT.
Happy caking! 🍰

Nenhum comentário:

Postar um comentário