Code Coverage su .NET Core con Coverlet e ReportGenerator

Inserire nel processo di sviluppo software la pratica dei Test - soprattutto se si sta lavorando ad un progetto di una certa portata - è un'ottima abitudine che incrementa notevolmente il livello di qualità e manutenibilità della soluzione stessa, e ci mette al riparo da situazioni inaspettate che possono sempre accadere.
Purtroppo questa pratica è sempre stata vista come una "inutile perdita di tempo", che sottrae giornate di lavoro che si potrebbero altrimenti dedicare allo sviluppo. Si tratta tuttavia di un'idea ormai superata: è infatti provato che il tempo speso ad invididuare e a correggere errori introdotti in fase di sviluppo, è superiore al tempo che si sarebbe dedicato a realizzare i test che avrebbero garantito un livello di qualità più elevato del prodotto.
Superato lo scoglio della volontà di "fare test" (ed è uno scoglio bello grosso, credetemi!), ci si trova spesso a dover decidere quali test fare: Unit Tests, Behavioral Tests, System Tests, End-to-end Tests, ecc. Non è mia intenzione in questo post fare una panoramica di ciascuno di essi: ci sono moltissimi articoli in rete che rispondono a questi quesiti. Voglio invece porre un quesito differente: "Quante righe del mio codice sono effettivamente sottoposte a tests?".
Per rispondere a questa domanda esistono una serie di strumenti che ormai da molti anni sono disponibili in versione "pay" o "free". Tutti sono riconosciuti sotto il nome di "Code Coverage Tools".
Nel mondo Java, Python e NodeJs fare "code coverage" è una pratica ben diffusa da parecchio tempo. Ma .NET Core è una piattaforma piuttosto recente, nonostante nasca dalla lunga esperienza Microsoft di .NET. Qui gli strumenti, soprattutto quelli "open", non sono moltissimi e sono ancora poco conosciuti alla maggior parte degli sviluppatori che si approcciano al mondo dei tests.
In questo post voglio descrivere la combinazione che quotidianamente utilizzo quando lavoro a progetti di una certa importanza. Per il mio tutorial mi avvarrò di CoverletReport Generator e output results in formato Cobertura.
Per prima cosa è necessario creare un progetto .NET Core da testare: niente di interessante, ma valido per il nostro scopo. Mi voglio sforzare di utilizzare gli strumenti da commandline per essere il più possibile "cross-platform", avvalendomi come editor di Visual Studio Code.
Premessa: come mi succede già da qualche tempo, ho scelto di realizzare questo tutorial su una macchina Linux, quindi tutti i percorsi delle cartelle e file di progetto utilizzano il "forward-slash" (/) invece che il "back-slash" di Windows (\); se deciderete di eseguire il tutto su Windows, ricordatevi quindi di invertire i separatori ;) …
Creiamo una cartella di lavoro (la mia l'ho chiamata con molta fantasia SampleCodeCoverage) e una soluzione vuota con il comando:
dotnet new sln
Nella stessa folder lanciamo i comandi per la creazione del progetto da testare ("SampleCodeCoverage") e il progetto di test realizzato con xUnit("SampleCodeCoverage.Tests"). Quindi aggiungiamo i due progetti alla soluzione e il progetto da testare al progetto di test, eseguiamo un restore dei pacchetti e un build della soluzione:
dotnet new classlib -n SampleCodeCoverage
dotnet new xunit -n SampleCodeCoverage.Tests
dotnet sln add ./SampleCodeCoverage/SampleCodeCoverage.csproj
dotnet sln add ./SampleCodeCoverage.Tests/SampleCodeCoverage.Tests.csproj
dotnet add ./SampleCodeCoverage.Tests/SampleCodeCoverage.Tests.csproj reference ./SampleCodeCoverage/SampleCodeCoverage.csproj
dotnet restore
dotnet build
A questo punto aggiungiamo un po' di codice: una semplice classe di utilità con 3 metodo banali: andremo a coprire con Unit Test solo 2 di questi. Inseriamo la classe statica "SampleCodeCoverageUtils" nel progetto "SampleCodeCoverage":
namespace SampleCodeCoverage
{
    public static class SampleCodeCoverageUtils
    {
        public static int DoSum(int first, int second)
        {
            return first + second;
        }

        public static int DoSubtract(int first, int second)
        {
            return first - second;
        }

        public static int DoMultiply(int first, int second)
        {
            return first * second;
        }
    }
}
Nel progetto "SampleCodeCoverage.Tests" aggiungiamo invece la classe di test, basata su xUnit, chiamata "SampleCodeCoverageUtilsTests":
using Xunit;

namespace SampleCodeCoverage.Tests
{
    public class SampleCoverageUtilsTests
    {
        [Fact]
        public void ShouldDoSumExecuteSum()
        {
            var result = SampleCodeCoverageUtils.DoSum(2, 3);
            Assert.Equal(5, result);

        }

        [Fact]
        public void ShouldDoSubtractExecuteSubtract()
        {
            var result = SampleCodeCoverageUtils.DoSubtract(3, 2);
            Assert.Equal(1, result);
        }
    }
}
Lanciando il comando di test di .NET Core dotnet test è immediatamente evidente come entrambi i test scritti soddisfano i requisiti (in gergo "sono in fase verde") e producono un output di questo genere:
Microsoft (R) Test Execution Command Line Tool Version 16.2.0-preview-20190606-02
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...

Test Run Successful.
Total tests: 2
     Passed: 2
 Total time: 1.6376 Seconds
A questo punto possiamo iniziare ad inserire i tools di code coverage, iniziando da Coverlet e dal suo adattatore per MSBuild. Sempre con la linea di comando nella folder della soluzione digitiamo:
dotnet add ./SampleCodeCoverage.Tests/SampleCodeCoverage.Tests.csproj package coverlet.msbuild
Dopo il ripristino da NuGet del pacchetto possiamo fare un check rapido per verificare se lo strumento è stato installato correttamente ed è attivo:
dotnet test /p:CollectCoverage=true
Se va tutto bene, dovrebbe essere mostrato il seguente output
Starting test execution, please wait...


Test Run Successful.
Total tests: 2
     Passed: 2
 Total time: 1.6655 Seconds

Calculating coverage result...
  Generating report '[...]/SampleCodeCoverage/SampleCodeCoverage.Tests/coverage.json'

+--------------------+--------+--------+--------+
| Module             | Line   | Branch | Method |
+--------------------+--------+--------+--------+
| SampleCodeCoverage | 66.66% | 100%   | 66.66% |
+--------------------+--------+--------+--------+

+---------+--------+--------+--------+
|         | Line   | Branch | Method |
+---------+--------+--------+--------+
| Total   | 66.66% | 100%   | 66.66% |
+---------+--------+--------+--------+
| Average | 66.66% | 100%   | 66.66% |
+---------+--------+--------+--------+
E' evidente che solo il 66% delle linee di codice del nostro programma sono sotto copertura da parte dei test; con uno sguardo veloce al codice sorgente che abbiamo realizzato solo qualche linea più sopra, è chiaro che il metodo DoMultiply e il suo contenuto non sono coperte, mentre i metodi DoSumDoSubtract lo sono.
Però questa cosa la possiamo desumere dal fatto che abbiamo solo poche linee di codice nel nostro programma. Immaginatevi uno scenario normale, in cui il progetto a cui state lavorando è composto da decine o addirittura centinaia di migliaia di righe di codice. Come potreste essere in grado di determinare quali solo le porzioni di codice coperte da test e quali no? Per fortuna qui ci viene in aiuto un altro prezioso strumento che - con l'aiuto di alcune opzioni di Coverlet - ci fornisce questa visione più dettaglitata.
Prima di tutto variamo leggermente il comando di test lanciato poco fa in modo da ridirezionare l'output nella posizione e nel formato:
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput="./TestResults/"
Il parametro opzionale CoverletOutputFormat permette di variare la tipogia di formato di uscita; nel nostro caso useremo il provider per Cobertura, ma sono supportate altre opzioni, come si può apprendere dalla documentazione ufficiale.
Il secondo parametro CoverletOutput serve per ridirezionare il percorso di generazione del file di output: in questo caso lo faremo in una directory che non sia la root del progetto di test, ma in una sotto-directory dedicata.
Dopo l'esecuzione il nuovo file di output con la code coverage dovrebbe essere stato generato nel percorso: ./SampleCodeCoverage.Tests/TestResults/coverage.cobertura.xml.
L'output a linea di comando non è male per avere una overview della percentuale di codice attualmente sotto copertura di test. Tuttavia non è sufficiente - soprattutto per uno sviluppatore - per comprendere nel dettaglio quali sono in punti in cui deve migliorare, facendo nel contempo le giuste valutazioni. Per questo motivo introduciamo nell'equazione un altro strumento più "user friendly": Report Generator, ottimo strumento realizzato da Daniel Palme.
Per utilizzare questo strumento è sufficiente aggiungere una sorta di "plugin" alla commandline di .NET Core. E' semplicissimo: basta aggiungere queste tre righe di codice al file .csproj del progetto di test della vostra soluzione:
<ItemGroup>
   <DotNetCliToolReference Include="dotnet-reportgenerator-cli" Version="4.2.15" />
</ItemGroup>
Nota: al momento della scrittura di questo articolo la versione del tool è la 4.2.15, ma potete verificare l'ultima versione direttamente sulla pagina NuGet di riferimento (https://www.nuget.org/packages/dotnet-reportgenerator-cli/).
Eseguiamo un restore del plugin con il solito comando dotnet restore e siamo pronti per il prossimo step.
Consiglio: nonostante sia possibile referenziare reportgenerator usando il comando dotnet add package ..., vi sconsiglio di farne uso perchè non vi permette di utilizzare le sue funzionalità da linea di comando.
A differenza dei precedenti comandi, a questo punto dobbiamo spostarci nella folder del progetto di test. La motivazione è semplice: abbiamo referenziato l'estensione di "dotnet" unicamente sul progetto "SampleCodeCoverage.Tests" e quindi solo in quel contesto potrà essere utilizzata.
Lanciamo quindi il seguente comando che, come potete notare, specifica la posizione precisa del file di code coverage generato precedentemente (TestResults/coverage.cobertura.xml), e definisce la folder target in cui il report sarà generato (TestResults/html).
dotnet reportgenerator "-reports:TestResults/coverage.cobertura.xml" "-targetdir:TestResults/html" -reporttypes:HTML;
L'ultimo parametro reporttypes permette di specificare la tipologia di formato del report. Nel nostro caso, per semplicità di consultazione, abbiamo optato per un semplice HTML, ma lo strumento supporta una quantità considerevole di differenti formati: https://github.com/danielpalme/ReportGenerator#supported-input-and-output-file-formats.
Ad esecuzione avvenuta, nella folder target TestResults/html saranno presenti i file HTML con i dettagli della code coverage: basta lanciare il file index.html per accedere a tutte le informazioni:

Subito notiamo come il 66% di coverage dell'intero progetto è evidenziato anche in questo caso. Ma la cosa veramente interessante è che il nostro report è completamente interattivo: possiamo sfruttare gli hyperlink del formato HTML per navigare i singoli artefatti sottoposti a test. Cliccando quindi su SampleCodeCoverage.SampleCodeCoverageUtils possiamo accedere alla pagina di dettaglio:

Qui sono riportati i singoli metodi coperti da test, evidenziate le righe di codice sotto code coverage e le righe dei metodi che invece non sono testate.
Il tutto è molto chiaro e facilmente navigabile. E per generare un nuovo report aggiornato è sufficiente ricordarsi i due passaggi chiave:
1) Generare il file di output usando Coverlet (formato "Cobertura", nel nostro caso) 2) Lanciare Report Generator dalla cartella con il progetto di test
Esiste anche la possibilità di installare sia Coverlet che Report Generator come strumenti globali, accessibili da qualunque folder del vostro environment senza che gli stessi siano installati come packages oppure come dipendenze.
Se per Coverlet non è la ritengo una cosa particolarmente utile (è possibile comunque attivarla tramite il comando dotnet tool install --global coverlet.console), per Report Generator può essere estremamente comodo, soprattutto in contesti di automazione. Lanciamo quindi il seguente comando:
dotnet tool install --global dotnet-reportgenerator-globaltool
Se va tutto bene il comando reportgenerator dovrebbe essere attivo e, partendo dalla root-folder della nostra soluzione possiamo generare nuovamente il report HTML usando:
reportgenerator "-reports:./SampleCodeCoverage.Tests/TestResults/coverage.cobertura.xml" "-targetdir:./SampleCodeCoverage.Tests/TestResults/html" -reporttypes:HTML;
Fare code coverage non è molto più complesso che fare dei normali test. Tuttavia strumenti come quelli descritti in questo articolo dovrebbero farci comprendere che una copertura del 100% delle nostre linee di codice di progetto è più una utopia che un obiettivo difficile da raggiungere. Quindi, come sempre, usiamo il buonsenso e sforziamoci di avere strumenti di "quality assurance" che coprono le parti importanti del nostro sistema, accettando che qualche errore fa parte del nostro mestiere ed è un rischio che possiamo correre.
Happy covering…
M.

Commenti

Post popolari in questo blog

Cancellazione fisica vs cancellazione logica dei dati

RESTful Stress: misurare le performance di un servizio REST

Load tests, Stress tests e performance di un servizio REST