6. Effettuare Unit Test

Da novembre 2007 richiediamo che tutte le nuove funzionalità che vanno in master siano accompagnate da una Unit Test. Attualmente abbiamo limitato questo requisito a qgis_core, ed estenderemo questo requisito ad altre parti del codice base una volta che le persone avranno familiarizzato con le procedure per i test unit spiegate nelle sezioni che seguono.

6.1. Il framework di test di QGIS: una panoramica

Gli Unit Test vengono eseguiti utilizzando una combinazione di QTestLib (la libreria di test di Qt) e CTest (un framework per la compilazione e l’esecuzione di test come parte del processo di compilazione di CMake). Diamo una panoramica del processo prima di addentrarci nei dettagli:

  1. C’è del codice che si vuole testare, ad esempio una classe o una funzione. I sostenitori della programmazione estrema suggeriscono che il codice non dovrebbe essere ancora stato scritto quando si inizia a costruire i test, e poi, man mano che si implementa il codice, si può immediatamente convalidare ogni nuova parte funzionale aggiunta con i test. In pratica, sarà probabilmente necessario scrivere test per il codice preesistente in QGIS, dato che stiamo iniziando con un framework di test ben dopo che gran parte della logica dell’applicazione è già stata implementata.

  2. Tu crei un unit test. Questo avviene sotto <QGIS Source Dir>/tests/src/core nel caso della libreria core. Il test è fondamentalmente un client che crea un’istanza di una classe e chiama alcuni metodi di quella classe. Verrà controllato il ritorno di ogni metodo per assicurarsi che corrisponda al valore atteso. Se una qualsiasi delle chiamate fallisce, il test fallisce.

  3. Includi le macro di QtTestLib nella tua classe di test. Queste macro vengono elaborate dal compilatore di meta-oggetti Qt (moc) ed espandono la classe di test in un’applicazione eseguibile.

  4. Aggiungi una sezione al file CMakeLists.txt nella cartella dei test che compilerà il tuo test.

  5. Assicurati di aver abilitato ``ENABLE_TESTING”” in ccmake / cmakesetup. Questo assicura che i test vengano effettivamente compilati quando tu digiti make.

  6. Puoi opzionalmente aggiungere i dati di test a <QGIS Source Dir>/tests/testdata se il tuo test è guidato dai dati (ad esempio, se deve caricare uno shapefile). Questi dati di test dovrebbero essere il più piccoli possibile e, ove possibile, si dovrebbero usare i set di dati esistenti. I test non dovrebbero mai modificare questi dati in loco, ma piuttosto farne una copia temporanea da qualche parte, se necessario.

  7. Compili i tuoi sorgenti e li installi. Lo fai usando la normale procedura make && (sudo) make install.

  8. Esegui i tuoi test. Di solito lo si fa semplicemente eseguendo make test dopo il passo make install, anche se verranno spiegati altri approcci che offrono un controllo più preciso sull’esecuzione dei test.

Dopo questa panoramica, ci addentreremo in un po” di dettagli. Abbiamo già fatto gran parte della configurazione in CMake e in altri punti dell’albero dei sorgenti, quindi tutto ciò che dovete fare è la parte più semplice: scrivere unit tests!

6.2. Realizzare una unit test

Creare una unit test è facile: basta che creai un singolo file .cpp (non viene usato nessun file .h) e implementi tutti i metodi di test come metodi pubblici che restituiscono void. Nella sezione che segue useremo una semplice classe di test per QgsRasterLayer per illustrare la situazione. Per convenzione, chiameremo i nostri test con lo stesso nome della classe che stanno testando, ma con il prefisso «Test». Quindi l’implementazione del nostro test va in un file chiamato testqgsrasterlayer.cpp e la classe stessa sarà TestQgsRasterLayer. Per prima cosa aggiungiamo il nostro banner di copyright standard:

/***************************************************************************
 testqgsvectorfilewriter.cpp
 --------------------------------------
  Date : Friday, Jan 27, 2015
  Copyright: (C) 2015 by Tim Sutton
  Email: [email protected]
 ***************************************************************************
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 ***************************************************************************/

Quindi iniziamo i nostri include necessari per i test che intendiamo eseguire. C’è un include speciale che tutti i test dovrebbero avere:

#include <QtTest/QtTest>

Oltre a ciò, continua a implementare la tua classe come di consueto, inserendo tutte le intestazioni di cui hai bisogno:

//Qt includes...
#include <QObject>
#include <QString>
#include <QObject>
#include <QApplication>
#include <QFileInfo>
#include <QDir>

//qgis includes...
#include <qgsrasterlayer.h>
#include <qgsrasterbandstats.h>
#include <qgsapplication.h>

Poiché stiamo combinando sia la dichiarazione della classe che l’implementazione in un unico file, la dichiarazione della classe viene dopo. Cominciamo con la nostra documentazione doxygen. Ogni caso di test dovrebbe essere adeguatamente documentato. Usiamo la direttiva doxygen ingroup, in modo che tutti gli UnitTest appaiano come un modulo nella documentazione Doxygen generata. Dopo di che viene una breve descrizione dello unit test e della classe, che deve ereditare da QObject e includere la macro Q_OBJECT.

/** \ingroup UnitTests
 * This is a unit test for the QgsRasterLayer class.
 */

class TestQgsRasterLayer: public QObject
{
    Q_OBJECT

Tutti i nostri metodi di test sono implementati come slot privati. Il framework QtTest chiamerà in sequenza ogni metodo privato della classe di test. Ci sono quattro metodi “speciali” che, se implementati, saranno chiamati all’inizio del test unitario (initTestCase) e alla fine del test unitario (cleanupTestCase). Prima che ogni metodo di test venga chiamato, viene richiamato il metodo init() e dopo che ogni metodo di test è stato chiamato viene richiamato il metodo cleanup(). Questi metodi sono utili perché consentono di allocare e ripulire le risorse prima dell’esecuzione di ogni test e della UnitTest nel suo complesso..

private slots:
  // will be called before the first testfunction is executed.
  void initTestCase();
  // will be called after the last testfunction was executed.
  void cleanupTestCase(){};
  // will be called before each testfunction is executed.
  void init(){};
  // will be called after every testfunction.
  void cleanup();

Seguono i tuoi metodi di test, che non devono accettare alcun parametro e devono restituire void. I metodi saranno chiamati in ordine di dichiarazione. Qui implementiamo due metodi che illustrano due tipi di test.

Nel primo caso, vogliamo verificare in generale se le varie parti della classe funzionano, e possiamo usare un approccio di test funzionale. Anche in questo caso, i programmatori più esigenti sono favorevoli a scrivere questi test prima di implementare la classe. Poi, man mano che si procede con l’implementazione della classe, si eseguono iterativamente gli unit test. Sempre più funzioni di test dovrebbero essere completate con successo man mano che il lavoro di implementazione della classe progredisce e, quando l’intero unit test passa, la nuova classe è terminata ed è ora completa con un metodo ripetibile per convalidarla.

In genere i tuoi unit test coprono solo l’API pubblica della classe e normalmente non devi scrivere test per gli accessor e i mutator. Se dovesse accadere che un accessor o un mutator non funziona come previsto, si dovrebbe normalmente implementare un regression test per verificarlo.

//
// Functional Testing
//

/** Check if a raster is valid. */
void isValid();

// more functional tests here ...

6.2.1. Implementazione di un test di regressione

Di seguito, implementiamo i nostri test di regressione. I test di regressione devono essere implementati per replicare le condizioni di un particolare bug. Ad esempio:

  1. Abbiamo ricevuto una segnalazione via e-mail secondo la quale il conteggio delle celle per i raster era sbagliato di 1 unità, con la conseguente perdita di tutte le statistiche per le bande di raster.

  2. Abbiamo aperto una segnalazione di bug (ticket #832)

  3. Abbiamo creato un test di regressione che replica il bug utilizzando un piccolo set di dati di prova (un raster 10x10).

  4. Abbiamo eseguito il test, verificando che era effettivamente fallito (il numero di celle era 99 invece di 100).

  5. Poi siamo andati a correggere il bug e abbiamo rieseguito la unit test e il test di regressione è passato. Abbiamo fatto il commit del test di regressione insieme alla correzione del bug. Ora, se in futuro qualcuno dovesse ripetere l’errore nel codice sorgente, potremo identificare immediatamente che il codice è stato regredito.

    Meglio ancora, prima di apportare qualsiasi modifica in futuro, l’esecuzione dei test assicurerà che le nostre modifiche non abbiano effetti collaterali inaspettati, come l’interruzione di funzionalità esistenti.

C’è un altro vantaggio dei test di regressione: possono farvi risparmiare tempo. Se hai mai risolto un bug che comportava l’apporto di modifiche al sorgente, l’esecuzione dell’applicazione e l’esecuzione di una serie di passaggi complicati per replicare il problema, ti sarà subito chiaro che la semplice implementazione del test di regressione prima di risolvere il bug ti consentirà di automatizzare i test per la risoluzione del bug in modo efficiente.

Per implementare il test di regressione, devi seguire la naming convention di regression<TicketID> per le tue funzioni di test. Se non esiste un ticket per la regressione, è necessario crearne uno. L’uso di questo approccio consente alla persona che esegue un test di regressione fallito di andare facilmente a cercare ulteriori informazioni.

//
// Regression Testing
//

/** This is our second test case...to check if a raster
 *  reports its dimensions properly. It is a regression test
 *  for ticket #832 which was fixed with change r7650.
 */
void regression832();

// more regression tests go here ...

Infine, nella dichiarazione della classe di test puoi dichiarare privatamente tutti i membri di dati e i metodi di aiuto di cui lo unit test può avere bisogno. Nel nostro caso, dichiareremo un QgsRasterLayer * che può essere usato da qualsiasi metodo di test. Il layer raster sarà creato nella funzione initTestCase(), che viene eseguita prima di ogni altro test, e poi distrutto con cleanupTestCase(), che viene eseguita dopo tutti i test. Dichiarando privatamente i metodi di aiuto (che possono essere chiamati da varie funzioni di test), si può fare in modo che non vengano eseguiti automaticamente dall’eseguibile QTest che viene creato quando si compila il test.

  private:
    // Here we have any data structures that may need to
    // be used in many test cases.
    QgsRasterLayer * mpLayer;
};

Questo chiude la nostra dichiarazione di classe. L’implementazione è semplicemente inline nello stesso file più in basso. Prima le nostre funzioni di init e cleanup:

void TestQgsRasterLayer::initTestCase()
{
  // init QGIS's paths - true means that all path will be inited from prefix
  QString qgisPath = QCoreApplication::applicationDirPath ();
  QgsApplication::setPrefixPath(qgisPath, TRUE);
#ifdef Q_OS_LINUX
  QgsApplication::setPkgDataPath(qgisPath + "/../share/qgis");
#endif
  //create some objects that will be used in all tests...

  std::cout << "PrefixPATH: " << QgsApplication::prefixPath().toLocal8Bit().data() << std::endl;
  std::cout << "PluginPATH: " << QgsApplication::pluginPath().toLocal8Bit().data() << std::endl;
  std::cout << "PkgData PATH: " << QgsApplication::pkgDataPath().toLocal8Bit().data() << std::endl;
  std::cout << "User DB PATH: " << QgsApplication::qgisUserDbFilePath().toLocal8Bit().data() << std::endl;

  //create a raster layer that will be used in all tests...
  QString myFileName (TEST_DATA_DIR); //defined in CmakeLists.txt
  myFileName = myFileName + QDir::separator() + "tenbytenraster.asc";
  QFileInfo myRasterFileInfo ( myFileName );
  mpLayer = new QgsRasterLayer ( myRasterFileInfo.filePath(),
  myRasterFileInfo.completeBaseName() );
}

void TestQgsRasterLayer::cleanupTestCase()
{
  delete mpLayer;
}

La funzione init illustra un paio di cose interessanti.

  1. È necessario impostare manualmente il percorso dei dati dell’applicazione QGIS in modo che le risorse come srs.db possano essere trovate correttamente.

  2. In secondo luogo, si tratta di un test guidato dai dati, quindi è necessario fornire un modo per individuare genericamente il file tenbytenraster.asc. Questo è stato ottenuto utilizzando la definizione del compilatore TEST_DATA_PATH. La definizione viene creata nel file di configurazione CMakeLists.txt in <QGIS Source Root>/tests/CMakeLists.txt ed è disponibile per tutti gli unit test di QGIS. Se hai bisogno di dati di prova per il tuo test, esegui il commit sotto <QGIS Source Root>/tests/testdata. Si dovrebbe inserire qui solo insiemi di dati molto piccoli. Se il test deve modificare i dati del test, è necessario farne prima una copia.

Qt fornisce anche altri interessanti meccanismi per i test basati sui dati; se sei interessato a saperne di più sull’argomento, consulta la documentazione di Qt.

Diamo quindi un’occhiata al nostro test funzionale. Il test isValid() verifica semplicemente che il layer raster sia stato caricato correttamente nel caso initTestCase. QVERIFY è una macro Qt che si può usare per valutare una condizione di test. Esistono altre macro Qt da utilizzare nei tuoi test, tra cui:

  • QCOMPARE ( actual, expected )

  • QEXPECT_FAIL ( dataIndex, comment, mode )

  • QFAIL ( message )

  • QFETCH ( type, name )

  • QSKIP ( description, mode )

  • QTEST ( actual, testElement )

  • QTEST_APPLESS_MAIN ( TestClass )

  • QTEST_MAIN ( TestClass )

  • QTEST_NOOP_MAIN ()

  • QVERIFY2 ( condition, message )

  • QVERIFY ( condition )

  • QWARN ( message )

Alcune di queste macro sono utili solo quando si utilizza il framework Qt per i test orientati ai dati (si veda la documentazione Qt per maggiori dettagli).

void TestQgsRasterLayer::isValid()
{
  QVERIFY ( mpLayer->isValid() );
}

Normalmente i tuoi test funzionali devono coprire tutte le funzionalità delle API pubbliche delle classi, se possibile. Una volta terminati i test funzionali, possiamo esaminare il nostro esempio di test di regressione.

Poiché il problema del bug #832 è un conteggio errato delle celle, per scrivere il nostro test è sufficiente usare QVERIFY per verificare che il conteggio delle celle corrisponda al valore previsto:

void TestQgsRasterLayer::regression832()
{
  QVERIFY ( mpLayer->getRasterXDim() == 10 );
  QVERIFY ( mpLayer->getRasterYDim() == 10 );
  // regression check for ticket #832
  // note getRasterBandStats call is base 1
  QVERIFY ( mpLayer->getRasterBandStats(1).elementCountInt == 100 );
}

Con tutte le opzioni di unit test implementate, c’è un’ultima cosa da aggiungere alla nostra classe di test:

QTEST_MAIN(TestQgsRasterLayer)
#include "testqgsrasterlayer.moc"

Lo scopo di queste due linee è segnalare a Qt’s moc che si tratta di un QtTest (genererà un metodo main che a sua volta chiamerà ogni funzione di test. L’ultima riga è l’include per i sorgenti generati da MOC. Devi sostituire testqgsrasterlayer con il nome della tua classe in minuscolo.

6.3. Confronto delle immagini per i test di visualizzazione

La visualizzazione di immagini su ambienti diversi può produrre lievi differenze dovute a implementazioni specifiche della piattaforma (ad esempio, diversi algoritmi di visualizzazione e antialiasing dei caratteri), ai caratteri disponibili sul sistema e ad altre ragioni sconosciute.

Quando un test di visualizzazione viene eseguito su Travis e fallisce, cerca il link dash in fondo al log di Travis. Questo link porta a una pagina cdash in cui puoi vedere le immagini visualizzate rispetto a quelle attese, insieme a un’immagine di «differenza» che evidenzia in rosso tutti i pixel che non corrispondono all’immagine di riferimento.

Il sistema di unit test di QGIS supporta l’aggiunta di immagini «maschera», utilizzate per indicare quando un’immagine visualizzata può differire dall’immagine di riferimento. Un’immagine maschera è un’immagine (con lo stesso nome dell’immagine di riferimento, ma con il suffisso _mask.png) e deve avere le stesse dimensioni dell’immagine di riferimento. In un’immagine maschera i valori dei pixel indicano quanto il singolo pixel può differire dall’immagine di riferimento, quindi un pixel nero indica che il pixel nell’immagine visualizzata deve corrispondere esattamente allo stesso pixel nell’immagine di riferimento. Un pixel con RGB 2, 2, 2 significa che l’immagine visualizzata può variare fino a 2 nei suoi valori RGB rispetto all’immagine di riferimento, mentre un pixel completamente bianco (255, 255, 255) significa che il pixel viene effettivamente ignorato quando si confrontano le immagini previste e quelle visualizzate.

Uno script di utilità per generare immagini di maschere è disponibile come scripts/generate_test_mask_image.py. Questo script si usa passandogli il percorso di un’immagine di riferimento (ad esempio tests/testdata/control_images/annotations/expected_annotation_fillstyle/expected_annotation_fillstyle.png) e il percorso dell’immagine visualizzata.

Per esempio

scripts/generate_test_mask_image.py tests/testdata/control_images/annotations/expected_annotation_fillstyle/expected_annotation_fillstyle.png /tmp/path_to_rendered_image.png

Puoi abbreviare il percorso dell’immagine di riferimento passando una parte parziale del nome del test, ad es.

scripts/generate_test_mask_image.py annotation_fillstyle /tmp/path_to_rendered_image.png

(Questa scorciatoia funziona solo se viene trovata una sola immagine di riferimento corrispondente. Se vengono trovate più corrispondenze, è necessario fornire il percorso completo dell’immagine di riferimento).

Lo script accetta anche gli url http per l’immagine visualizzata, quindi puoi copiare direttamente un url dell’immagine visualizzata dalla pagina dei risultati di cdash e passarlo allo script.

Presta attenzione quando si generano le immagini della maschera: dovresti sempre visualizzare l’immagine della maschera generata e controllare le aree bianche nell’immagine. Poiché questi pixel vengono ignorati, assicurati che queste immagini bianche non coprano porzioni importanti dell’immagine di riferimento, altrimenti il tuo unit test sarà privo di significato!

Analogamente, puoi «oscurare» manualmente parti della maschera se vuoi deliberatamente escluderle dal test. Questo può essere utile, ad esempio, per i test che combinano la visualizzazione di simboli e testo (come i test delle legende), quando unit test non è progettato per verificare il testo visualizzato e non si vuole che il test sia soggetto alle differenze di visualizzazione del testo tra le piattaforme.

Per confrontare le immagini negli unit test QGIS dovresti utilizzare la classe QgsMultiRenderChecker o una delle sue sottoclassi.

Per migliorare la robustezza dei test, ecco alcuni suggerimenti:

  1. Disattiva l’antialiasing, se possibile, per ridurre al minimo le differenze di visualizzazione tra le diverse piattaforme.

  2. Assicurati che le tue immagini di riferimento siano «corpose»… cioè non abbiano linee larghe 1 px o altre caratteristiche sottili, e usa caratteri grandi e in grassetto (si consiglia di usare 14 punti o più).

  3. A volte i test generano immagini di dimensioni leggermente diverse (ad esempio i test di visualizzazione delle legende, in cui le dimensioni dell’immagine dipendono dalle dimensioni di visualizzazione dei caratteri, che sono soggette a differenze tra le piattaforme). Per tenerne conto, si può usare QgsMultiRenderChecker::setSizeTolerance() e specificare il numero massimo di pixel di differenza tra la larghezza e l’altezza dell’immagine visualizzata e l’immagine di riferimento.

  4. Non utilizzare sfondi trasparenti nelle immagini di riferimento (CDash non li supporta). Utilizzare invece QgsMultiRenderChecker::drawBackground() per disegnare un motivo a scacchiera per lo sfondo dell’immagine di riferimento.

  5. Quando sono richiesti i font, utilizza il font specificato in QgsFontUtils::standardTestFontFamily() («QGIS Vera Sans»).

Se travis segnala errori per le nuove immagini (ad esempio a causa di differenze di antialiasing o di font), lo script parse_dash_results.py può essere d’aiuto quando si aggiornano le maschere di prova locali.

6.4. Aggiunta di tuoi unit test a CMakeLists.txt

Per aggiungere tuoi unit test al sistema di compilazione è sufficiente modificare il file CMakeLists.txt nella cartella dei test, clonare uno dei blocchi di test esistenti e sostituirvi il nome della tua classe di test. Per esempio:

# QgsRasterLayer test
ADD_QGIS_TEST(rasterlayertest testqgsrasterlayer.cpp)

6.4.1. Spiegazione della macro ADD_QGIS_TEST

Esamineremo brevemente queste linee per spiegare cosa fanno, ma se non sei interessato, esegui semplicemente il procedimento spiegato nella sezione precedente.

MACRO (ADD_QGIS_TEST testname testsrc)
SET(qgis_${testname}_SRCS ${testsrc} ${util_SRCS})
SET(qgis_${testname}_MOC_CPPS ${testsrc})
QT4_WRAP_CPP(qgis_${testname}_MOC_SRCS ${qgis_${testname}_MOC_CPPS})
ADD_CUSTOM_TARGET(qgis_${testname}moc ALL DEPENDS ${qgis_${testname}_MOC_SRCS})
ADD_EXECUTABLE(qgis_${testname} ${qgis_${testname}_SRCS})
ADD_DEPENDENCIES(qgis_${testname} qgis_${testname}moc)
TARGET_LINK_LIBRARIES(qgis_${testname} ${QT_LIBRARIES} qgis_core)
SET_TARGET_PROPERTIES(qgis_${testname}
PROPERTIES
# skip the full RPATH for the build tree
SKIP_BUILD_RPATHTRUE
# when building, use the install RPATH already
# (so it doesn't need to relink when installing)
BUILD_WITH_INSTALL_RPATH TRUE
# the RPATH to be used when installing
INSTALL_RPATH ${QGIS_LIB_DIR}
# add the automatically determined parts of the RPATH
# which point to directories outside the build tree to the install RPATH
INSTALL_RPATH_USE_LINK_PATH true)
IF (APPLE)
# For macOS, the executable must be at the root of the bundle's executable folder
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX})
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/qgis_${testname})
ELSE (APPLE)
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/bin/qgis_${testname})
ENDIF (APPLE)
ENDMACRO (ADD_QGIS_TEST)

Vediamo un po” più in dettaglio le singole linee. Per prima cosa definiamo l’elenco dei sorgenti per il nostro test. Poiché abbiamo un solo file sorgente (seguendo la metodologia descritta in precedenza, in cui la dichiarazione della classe e la definizione sono nello stesso file), si tratta di una semplice istruzione:

SET(qgis_${testname}_SRCS ${testsrc} ${util_SRCS})

Poiché la nostra classe di test deve essere eseguita attraverso il compilatore di meta-oggetti di Qt (moc), dobbiamo fornire un paio di linee per far sì che ciò avvenga:

SET(qgis_${testname}_MOC_CPPS ${testsrc})
QT4_WRAP_CPP(qgis_${testname}_MOC_SRCS ${qgis_${testname}_MOC_CPPS})
ADD_CUSTOM_TARGET(qgis_${testname}moc ALL DEPENDS ${qgis_${testname}_MOC_SRCS})

Poi diciamo a cmake che deve creare un eseguibile dalla classe di test. Ricordiamo che nella sezione precedente, nell’ultima riga dell’implementazione della classe, abbiamo incluso gli output di moc direttamente nella nostra classe di test, in modo da dotarla (tra le altre cose) di un metodo main, così che la classe possa essere compilata come eseguibile:

ADD_EXECUTABLE(qgis_${testname} ${qgis_${testname}_SRCS})
ADD_DEPENDENCIES(qgis_${testname} qgis_${testname}moc)

Poi bisogna specificare le dipendenze dalle librerie. Al momento, le classi sono state implementate con una dipendenza di tipo catch-all QT_LIBRARIES, ma lavoreremo per sostituirla con le librerie Qt specifiche di cui ogni classe ha bisogno. Naturalmente, devi collegare anche le librerie qgis pertinenti, come richiesto dal tuo unit test.

TARGET_LINK_LIBRARIES(qgis_${testname} ${QT_LIBRARIES} qgis_core)

Poi diciamo a cmake di installare i test nello stesso posto dei binari di qgis. Questo è un aspetto che intendiamo rimuovere in futuro, in modo che i test possano essere eseguiti direttamente dall’albero dei sorgenti.

SET_TARGET_PROPERTIES(qgis_${testname}
PROPERTIES
# skip the full RPATH for the build tree
SKIP_BUILD_RPATHTRUE
# when building, use the install RPATH already
# (so it doesn't need to relink when installing)
BUILD_WITH_INSTALL_RPATH TRUE
# the RPATH to be used when installing
INSTALL_RPATH ${QGIS_LIB_DIR}
# add the automatically determined parts of the RPATH
# which point to directories outside the build tree to the install RPATH
INSTALL_RPATH_USE_LINK_PATH true)
IF (APPLE)
# For macOS, the executable must be at the root of the bundle's executable folder
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX})
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/qgis_${testname})
ELSE (APPLE)
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/bin/qgis_${testname})
ENDIF (APPLE)

Infine il precedente usa ADD_TEST per registrare il test con cmake / ctest. Qui è dove avviene la magia migliore - registriamo la classe con ctest. Se ti ricordi nella panoramica che abbiamo dato all’inizio di questa sezione, stiamo usando sia QtTest che CTest insieme. Per ricapitolare, QtTest aggiunge un metodo principale alla tua unità di test e gestisce le chiamate ai tuoi metodi di test all’interno della classe. Fornisce anche alcune macro come QVERIFY che puoi usare per verificare il fallimento dei test usando delle condizioni. L’output di un test unitario di QtTest è un eseguibile che puoi eseguire dalla linea di comando. Tuttavia quando hai una suite di test e vuoi eseguire ogni eseguibile a turno, e meglio ancora integrare l’esecuzione dei test nel processo di compilazione, il CTest è ciò che usiamo.

6.5. Realizzare il tuo test unitario

Per costruire unit test devi solo assicurarti che sia ENABLE_TESTS=true nella configurazione di cmake. Ci sono due modi per farlo:

  1. Esegui ccmake .. (o cmakesetup .. sotto Windows) e imposta interattivamente il flag ENABLE_TESTS su ON.

  2. Aggiungi un flag da linea di comando a cmake, ad esempio cmake -DENABLE_TESTS=true ...

A parte questo, basta compilare QGIS come di consueto e anche i test dovrebbero essere compilati.

6.6. Eseguire i tuoi test

Il modo più semplice per eseguire i test è di farlo come parte del tuo normale processo di compilazione:

make && make install && make test

Il comando make test richiamerà CTest che eseguirà ogni test registrato con la direttiva ADD_TEST di CMake descritta sopra. L’output tipico di make test sarà simile a questo:

Running tests...
Start processing tests
Test project /Users/tim/dev/cpp/qgis/build
## 13 Testing qgis_applicationtest***Exception: Other
## 23 Testing qgis_filewritertest *** Passed
## 33 Testing qgis_rasterlayertest*** Passed

## 0 tests passed, 3 tests failed out of 3

The following tests FAILED:
## 1- qgis_applicationtest (OTHER_FAULT)
Errors while running CTest
make: *** [test] Error 8

Se un test fallisce, puoi usare il comando ctest per esaminare più da vicino il motivo del fallimento. Usa l’opzione -R per specificare una regex per i test che si vuoi eseguire e -V per ottenere l’output dettagliato:

$ ctest -R appl -V

Start processing tests
Test project /Users/tim/dev/cpp/qgis/build
Constructing a list of tests
Done constructing a list of tests
Changing directory into /Users/tim/dev/cpp/qgis/build/tests/src/core
## 13 Testing qgis_applicationtest
Test command: /Users/tim/dev/cpp/qgis/build/tests/src/core/qgis_applicationtest
********* Start testing of TestQgsApplication *********
Config: Using QTest library 4.3.0, Qt 4.3.0
PASS : TestQgsApplication::initTestCase()
PrefixPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/../
PluginPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//lib/qgis
PkgData PATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//share/qgis
User DB PATH: /Users/tim/.qgis/qgis.db
PASS : TestQgsApplication::getPaths()
PrefixPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/../
PluginPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//lib/qgis
PkgData PATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//share/qgis
User DB PATH: /Users/tim/.qgis/qgis.db
QDEBUG : TestQgsApplication::checkTheme() Checking if a theme icon exists:
QDEBUG : TestQgsApplication::checkTheme()
/Users/tim/dev/cpp/qgis/build/tests/src/core/..//share/qgis/themes/default//mIconProjectionDisabled.png
FAIL!: TestQgsApplication::checkTheme() '!myPixmap.isNull()' returned FALSE. ()
Loc: [/Users/tim/dev/cpp/qgis/tests/src/core/testqgsapplication.cpp(59)]
PASS : TestQgsApplication::cleanupTestCase()
Totals: 3 passed, 1 failed, 0 skipped
********* Finished testing of TestQgsApplication *********
-- Process completed
***Failed

## 0 tests passed, 1 tests failed out of 1

The following tests FAILED:
## 1- qgis_applicationtest (Failed)
Errors while running CTest

6.6.1. Esecuzione di singoli test

I test C++ sono normali applicazioni. Si possono eseguire dalla cartella di compilazione come qualsiasi altro eseguibile.

$ ./output/bin/qgis_dxfexporttest

********* Start testing of TestQgsDxfExport *********
Config: Using QtTest library 5.12.5, Qt 5.12.5 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 9.2.1 20190827 (Red Hat 9.2.1-1))
PASS   : TestQgsDxfExport::initTestCase()
PASS   : TestQgsDxfExport::testPoints()
PASS   : TestQgsDxfExport::testLines()
...
Totals: 19 passed, 4 failed, 0 skipped, 0 blacklisted, 612ms
********* Finished testing of TestQgsDxfExport *********

Questi test accettano anche command line arguments. In questo modo è possibile eseguire un sottoinsieme specifico di test:

$ ./output/bin/qgis_dxfexporttest testPoints
********* Start testing of TestQgsDxfExport *********
Config: Using QtTest library 5.12.5, Qt 5.12.5 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 9.2.1 20190827 (Red Hat 9.2.1-1))
PASS   : TestQgsDxfExport::initTestCase()
PASS   : TestQgsDxfExport::testPoints()
PASS   : TestQgsDxfExport::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 272ms
********* Finished testing of TestQgsDxfExport *********

6.6.2. Debug di unit test

Test C++

Per unit test in C++, QtCreator aggiunge automaticamente i target di esecuzione, in modo da poterli avviare dal debugger.

Se vai in Progetti e poi nella scheda Build & Run –> Desktop Run, puoi anche specificare i parametri della linea di comando che permetteranno di eseguire un sottoinsieme dei test all’interno di un file .cpp nel debugger.

Test Python

È anche possibile avviare unit test di Python da QtCreator con GDB. Per questo, devi andare in Projects e scegliere Run sotto Build & Run. Quindi aggiungi una nuova Run configuration con l’eseguibile /usr/bin/python3 e gli argomenti della linea di comando impostati sul percorso del file python del unit test, ad esempio /home/user/dev/qgis/QGIS/tests/src/python/test_qgsattributeformeditorwidget.py`.

Ora modifica anche il Run Environment e aggiungi 3 nuove variabili:

Variabile

Valore

PYTHONPATH

[build]/output/python/:[build]/output/python/plugins:[source]/tests/src/python

QGIS_PREFIX_PATH

[build]/output

LD_LIBRARY_PATH

[build]/output/lib

Sostituisci [build] con la cartella di compilazione e [source] con la cartella dei sorgenti.

6.6.3. Buon divertimento

Bene, questo conclude questa sezione sulla scrittura di unit test in QGIS. Speriamo che tu prenda l’abitudine di scrivere test per collaudare nuove funzionalità e per controllare le eventuali rettifiche. Alcuni aspetti del sistema di test (in particolare le parti CMakeLists.txt) sono ancora in fase di lavorazione in modo che il framework di test funzioni in modo veramente indipendente dalla piattaforma.