[Qt] Come misurare la copertura dei test su un progetto Qt con Gcov
Code Coverage
Un’attività importante al testing del codice è quella del code coverage, ovvero quanta percentuale del nostro codice è coperta da uno o più unit test. La strada per raggiungere dei test efficaci passa sicuramente dall’avere una percezione di quanto i test vadano in profondità nel testare il nostro codice. Attenzione, anche avere il 90% o addirittura il 100% della copertura non da indicazione dell’efficacia dei test stessi, ma fornisce una metrica di quanto i nostri test controllino sul nostro codice .
Come possiamo struttura un progetto per misurare la copertura dei test su Qt? Su Linux è estremamente semplice utilizzando GCC e uno dei suoi utility tool, GCov.
Creare il progetto
Cominciamo creando un semplice progetto di esempio così composto: un subdir project con due sotto-progetti contenenti in uno la nostra ipotetica libreria, nell’altro quello dei test. In un caso reale il sotto-progetto test dovrebbe essere a sua volta un subdir project contenente più progetti di test

Vediamo la composizione del .pro principale
TEMPLATE = subdirs SUBDIRS += \ lib \ test test.depends = lib DISTFILES += \ common.pri
Indichiamo la dipendenza in compilazione del progetto di test con quello delle librerie, e poi andiamo ad includere un file .pri comune per la compilazione dei sotto-progetti.
BUILD_DIR = $$PWD/build INCLUDE_DIR = $$BUILD_DIR/include LIB_DIR = $$BUILD_DIR/lib OBJ_DIR = $$BUILD_DIR/objs MOCS_DIR = $$BUILD_DIR/moc TEST_DIR = $$BUILD_DIR/test
Generare i report per il coverage
Adesso passiamo al progetto di libreria. Dobbiamo dire a GCC di generare i file di profilazione che verranno usati poi per generare le statistiche di coperatura. Niente di più semplice su Qt, è sufficiente aggiungere
QMAKE_CXXFLAGS += --coverage QMAKE_LFLAGS += --coverage
per far sì che in compilazione, oltre gli object file vengano generati anche dei file .gcno. La presenza di questi file ci mostrerà che tutto è stato fatto correttamente.
–coverage è un flag Qt per abilitare i file di report mantenendo la compatibilità con tutte le piattaforme. Su Linux e GCC equivale a compilare con i flag
gcc -fprofile-arcs -ftest-coverage ...
Il file pro della libreria si presenta quindi così
QT -= gui TEMPLATE = lib DEFINES += LIB_LIBRARY CONFIG += c++14 SOURCES += \ angle.cpp HEADERS += \ angle.h TARGET = mylib !include(../common.pri){ error(common.pri not found) } QMAKE_CXXFLAGS += --coverage QMAKE_LFLAGS += --coverage QMAKE_POST_LINK += mkpath($$INCLUDE_DIR) QMAKE_POST_LINK += $$QMAKE_COPY $$quote($$HEADERS) $$quote($$INCLUDE_DIR) $$escape_expand(\\n\\t) QMAKE_POST_LINK += $$QMAKE_COPY $$quote(../generate_coverage.sh) $$quote($$BUILD_DIR) $$escape_expand(\\n\\t)
Le direttive di QMAKE_POST_LINK servono a far sì che tutti i file necessari vengano spostati sulla directory build. In questo caso la libreria compilata, gli headers e il file di generazione del report che vedremo più avanti.
Testiamo qualcosa
Dobbiamo scrivere qualcosa da testare, no? Creiamo una classe in maniera motlo semplice in modo da poterla poi testare.
#ifndef ANGLE_H #define ANGLE_H #include <cmath> #include <QDebug> static constexpr float degToRad(float deg) { return deg * M_PI / 180.0; } static constexpr float radToDeg(float rad) { return rad * 180.0 / M_PI; } class Angle { public: enum Type { Deg, Rad }; //! Default angle is 0.0 deg Angle(); Angle(float value, Angle::Type type); float deg() const; float rad() const; bool isDeg() const; bool isRad() const; private: friend QDebug operator<<(QDebug dbg, const Angle& a); float _value; Type _type; }; QDebug operator<<(QDebug dbg, const Angle& a); #endif // ANGLE_H
#include "angle.h" Angle::Angle() : _value{0.0}, _type{Deg} { } Angle::Angle(float value, Angle::Type type) : _value{value}, _type{type} { } float Angle::deg() const { if(_type == Deg){ return _value; } return radToDeg(_value); } float Angle::rad() const { if(_type == Rad){ return _value; } return degToRad(_value); } bool Angle::isDeg() const { return _type == Deg; } bool Angle::isRad() const { return _type == Rad; } QDebug operator<<(QDebug dbg, const Angle& a) { return dbg << "Angle(" << a._value << (a.isDeg() ? "deg" : "rad") << ")"; }
Adesso non ci resta che scrivere la classe di test nel suo progetto
#include <QtTest> #include "angle.h" class Test : public QObject { Q_OBJECT private slots: void initTestCase_data(); void test_type(); void test_value(); private: float pi = M_PI; float p2 = M_PI_2; }; void Test::initTestCase_data() { QTest::addColumn<float>("degrees"); QTest::addColumn<float>("radians"); QTest::newRow("1") << 0.0f << 0.0f; QTest::newRow("2") << 180.0f << pi; QTest::newRow("3") << -90.0f << -p2; QTest::newRow("4") << 360.0f << 2.0f*pi; QTest::newRow("5") << 365.0f << 6.370451f; QTest::newRow("6") << -400.0f << -6.98131f; } void Test::test_type() { QFETCH_GLOBAL(float, degrees); QFETCH_GLOBAL(float, radians); Angle deg(degrees, Angle::Deg); Angle rad(radians, Angle::Rad); QCOMPARE(deg.isDeg(), true); QCOMPARE(deg.isRad(), false); QCOMPARE(rad.isDeg(), false); QCOMPARE(rad.isRad(), true); } void Test::test_value() { QFETCH_GLOBAL(float, degrees); QFETCH_GLOBAL(float, radians); Angle deg(degrees, Angle::Deg); Angle rad(radians, Angle::Rad); QCOMPARE(deg.deg(), degrees); QCOMPARE(deg.rad(), radians); QCOMPARE(rad.deg(), degrees); QCOMPARE(rad.rad(), radians); } QTEST_APPLESS_MAIN(Test) #include "tst_test.moc"
Tutto adesso e pronto. Potremo notare come compilando il progetto verranno generati i file .gcno e una volta lanciati i testi verranno aggiunti altrettanti file di tipo .gcda. Lanciamo i test.

Tutto è andato a buon fine. Adesso siamo pronti per potere generare le statistiche.
Generare il report
Abbiamo strutturato il progetto per fare in modo che tutti i file generati vadano nella cartella build. Spostiamoci lì e da terminale lanciamo il comando per generare il file di report
lcov -c -d objs -o mylib.info

Adesso generiamo un output leggibile per l’occhio umano tramite genhtml
genhtml mylib.info -o coverage

Aspetta un momento. 33.3% ? Sono discretamente sicuro di avere provato la maggior parte della classe Allora da dove arriva una percentuale così bassa? Il comando appena utilizzato ha generato una cartella chiamata coverage, dentro di essa troveremo il report in formato html. Diamo un’occhiata.
Ecco subito visibile l’inghippo. Il coverage effettivo dei test è al 77%, ma possiamo vedere che sono stati inseriti nel report delle cartelle relative a Qt e a gcc, che non fanno parte del progetto o dei test e che quindi falseranno le statistiche.
Filtriamo il report iniziale escludendo i path che non ci interessano generando un nuovo file di report
lcov -r "mylib.info" "*QtCore*" "/usr/*" -o "mylib-filtered.info"

Possiamo intuire subito dal risultato che le cartelle di troppo sono state rimosse. Il filtro usato è specifico per il progetto, ma si possono sempre utilizzare dei filtri più generici per rimuovere path dei file generati da Qt o dai test, più cartelle di sistema che possono essere linkate dal compilatore o librerie esterne. Questo ne è un esempio:
lcov -r "mylib.info" "*QtCore*" "*QtGui*" "*QtWidgets*" "*Qt*.framework*" "/usr/*" "*.moc" "*moc_*.cpp" -o "mylib-filtered.info"
Rigeneriamo il file html con il file report filtrato
lcov -r "mylib.info" "*QtCore*" "*QtGui*" "*QtWidgets*" "*Qt*.framework*" "/usr/*" "*.moc" "*moc_*.cpp" -o "mylib-filtered.info"
Lanciamo nuovamente il comando di gentml e otteniamo il nuovo report
Adesso vediamo solo la cartella che ci interessa. La percentuale di copertura è al 77.4%, accettabile secondo gcov che lo marca con un’icona gialla. Però questo sta ad indicare che abbiamo mancato di testare qualcosa, ma cosa? Navigando dentro la cartella lib e i singoli file possiamo vedere ogni singolo blocco e linea quante volte è stata eseguita da un test.


Eccolo lì, non abbiamo mai chiamato il costruttore di default. Inseriamo un test che copre questo caso
void Test::test_constructor() { Angle a; qDebug() << a; QCOMPARE(a.isDeg(), true); QCOMPARE(a.deg(), 0.0f); }
Se vi state chiedendo “ma è davvero necessario testare anche il costruttore di default?” Beh, sì. anzi, è la prima cosa che dovreste testare. In fondo state costruendo un oggetto, prima di verificare che i suoi metodi siano corretti, non sarà meglio accertarsi che le sue invarianti siano come quelle previste? E potremmo anche cominciare a parlare di come possa tornare utile testare tramite i type_traits i vari costruttori di default, ma non è lo scopo di questo articolo…
Altra passata di report…

…e voilà, adesso la classe è interamente testata.
Codice
Trovate il sorgente del progetto completo su questo repository.
Link utili