6. Tests unitaires

À partir de novembre 2007 nous exigeons que les nouvelles fonctionnalités de la branche master soient accompagnées d’un test unitaire. Nous avions initialement limité cette exigence à la partie qgis_core et nous allons étendre ce point aux autres parties du code une fois que les développeurs se seront familiarisés avec les procédures des tests unitaires, expliquées dans les sections qui suivent.

6.1. L’environnement de test de QGIS: un aperçu

Les tests unitaires sont effectués en utilisant une combinaison de QTestLib (la bibliothèque de tests Qt) et CTest (un cadre pour la compilation et l’exécution de tests dans le cadre du processus de construction CMake). Faisons un tour d’horizon du processus avant d’entrer dans les détails :

  1. Il y a un code que vous voulez tester, par exemple une classe ou une fonction. Les partisans de la programmation suggèrent que le code ne soit même pas encore écrit lorsque vous commencez à construire vos tests, et qu’ensuite, à mesure que vous implémentez votre code, vous puissiez immédiatement valider chaque nouvelle partie fonctionnelle que vous ajoutez avec votre test. En pratique, vous devrez probablement écrire des tests pour du code préexistant dans QGIS puisque nous commençons avec un cadre de test bien après qu’une grande partie de la logique applicative ait déjà été mise en œuvre.

  2. Créez un test unitaire. Tout se passe dans /tests/src/core dans le cas d’une bibliothèque du coeur du projet. Le test est essentiellement un client qui créé une instance de la classe et l’appelle avec des méthodes de classe. Cela permet de vérifier que le retour de chaque méthode renvoie bien la valeur attendue. Si un seul des appels échoue, le test sera également en échec.

  3. Incluez les macros QTestLib dans votre classe de test. Cette macro est prise en compte par le compilateur de méta-objets Qt (moc) et elle transformera votre classe de test en application exécutable.

  4. Ajoutez une section au fichier CMakeLists.txt dans le répertoire des tests qui construira votre test.

  5. Assurez-vous d’avoir la variable ENABLE_TESTING activée dans ccmake / cmakesetup. Cela permettra de s’assurer que vos tests seront compilés lorsque vous lancerez make.

  6. Vous pouvez éventuellement ajouter des données de test à `` <QGIS Source Dir>/tests/testdata`` si votre test est basé sur des données (par exemple, si vous devez charger un fichier de forme). Ces données de test doivent être aussi petites que possible et, dans la mesure du possible, vous devez utiliser les ensembles de données déjà existants. Vos tests ne doivent jamais modifier ces données in situ, mais plutôt en faire une copie temporaire quelque part si nécessaire.

  7. Compilez vos sources et installez. Vous pouvez le faire avec le traditionnel make && (sudo) make install.

  8. Vous faites vos tests. Cela se fait normalement en faisant simplement make test après l’étape make install, bien que nous expliquerons d’autres approches qui offrent un contrôle plus fin sur l’exécution des tests.

C’est en gardant cette vue d’ensemble à l’esprit que nous allons approfondir un peu les détails. Nous avons déjà fait une grande partie de la configuration pour vous dans CMake et à d’autres endroits de l’arbre des sources, donc tout ce que vous avez à faire, ce sont les parties faciles - écrire des tests unitaires !

6.2. Créer un test unitaire

La création d’un test unitaire est facile - en général, vous le ferez en créant un seul fichier .cpp (aucun fichier .h n’est utilisé) et en implémentant toutes vos méthodes de test comme des méthodes publiques qui renvoient nul. Nous utiliserons une simple classe de test pour « QgsRasterLayer » tout au long de la section qui suit pour illustrer notre propos. Par convention, nous donnerons à notre test le même nom que la classe qu’ils testent, mais avec le préfixe « Test ». Ainsi, notre implémentation de test va dans un fichier appelé testqgsrasterlayer.cpp et la classe elle-même sera TestQgsRasterLayer. Nous ajoutons d’abord notre bannière de 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.
 *
 ***************************************************************************/

Ensuite, nous commençons à préparer les éléments nécessaires aux tests que nous prévoyons de réaliser. Il y a un include spécial que tous les tests devraient avoir :

#include <QtTest/QtTest>

Ensuite, vous pouvez implémenter votre classe normalement en ajoutant les en-têtes dont vous pourrez avoir besoin:

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

Etant donné que nous combinons, dans un seul fichier, la déclaration et l’implémentation de la classe, nous ajoutons ensuite la déclaration de la classe. Nous ajoutons alors la documentation doxygen. Chaque test doit être correctement documenté. Nous utilisons la directive doxygen ingroup de manière à ce que tous les tests unitaires apparaissent dans un seul module dans la documentation générée par Doxygen. Vient ensuite une description résumée du test unitaire, la classe doit hériter de QObject et inclure la macro Q_OBJECT.

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

class TestQgsRasterLayer: public QObject
{
    Q_OBJECT

Toutes nos méthodes de test sont mises en œuvre sous forme de créneaux horaires privés. Le cadre QtTest appellera séquentiellement chaque méthode de créneau privé dans la classe de test. Il existe quatre méthodes “special” qui, si elles sont implémentées, seront appelées au début du test unitaire (initTestCase), à la fin du test unitaire (cleanupTestCase). Avant l’appel de chaque méthode de test, la méthode « init()`` sera appelée et après chaque méthode de test, la méthode « cleanup()`` sera appelée. Ces méthodes sont pratiques dans la mesure où elles vous permettent d’allouer et de nettoyer les ressources avant d’exécuter chaque test, et l’unité de test dans son ensemble.

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();

Viennent ensuite les méthodes d’essai, qui ne doivent pas prendre de paramètres et doivent être nulles. Les méthodes seront appelées dans l’ordre de leur déclaration. Nous mettons ici en œuvre deux méthodes qui illustrent deux types de test .

Dans le premier cas, nous voulons tester de manière générale si les différentes parties de la classe fonctionnent, nous pouvons utiliser une approche de test fonctionnel. Là encore, les programmeurs préconiseraient d’écrire ces tests avant de mettre en œuvre la classe. Ensuite, au fur et à mesure de l’implémentation de la classe, vous effectuez vos tests unitaires de manière itérative. De plus en plus de fonctions de test devraient se terminer avec succès au fur et à mesure de l’avancement de votre travail d’implémentation de la classe. Lorsque le test unitaire complet est réussi, votre nouvelle classe est terminée et est maintenant complétée avec un moyen (répétable) de la valider.

En général, vos tests unitaires ne couvrent que l’API publique de votre classe, et normalement vous n’avez pas besoin de faire des tests pour les accesseurs et les mutators. S’il arrivait qu’un accesseur ou un mutator ne fonctionne pas comme prévu, vous devriez normalement implémenter un regression test pour le vérifier.

//
// Functional Testing
//

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

// more functional tests here ...

6.2.1. Mise en œuvre d’un test de régression

Ensuite, nous mettons en œuvre nos tests de régression. Les tests de régression doivent être mis en œuvre pour reproduire les conditions d’un bogue particulier. Par exemple :

  1. Nous avons reçu par e-mail un rapport indiquant que le comptage des cellules par raster était décalé de 1, ce qui a faussé toutes les statistiques pour les bandes de raster.

  2. Nous avons ouvert un rapport de bogue (ticket #832)

  3. Nous avons créé un test de régression qui a reproduit le bogue en utilisant un petit ensemble de données de test (un raster 10x10).

  4. Nous avons effectué le test, en vérifiant qu’il avait bien échoué (le nombre de cellules était de 99 au lieu de 100).

  5. Ensuite, nous sommes allés corriger le bogue et nous avons refait le test unitaire et le test de régression a réussi. Nous avons effectué le test de régression en même temps que la correction du bogue. Maintenant, si quelqu’un le casse à nouveau dans le code source dans le futur, nous pouvons immédiatement identifier que le code a régressé.

    Mieux encore, avant d’apporter des modifications à l’avenir, nous effectuons des tests pour nous assurer que nos changements n’auront pas d’effets secondaires inattendus, comme la rupture de fonctionnalités existantes.

Il existe également une autre avancée offerte par les tests de non-régression: ils peuvent vous permettre de gagner du temps. Si vous avez déjà corrigé un bogue qui implique du changement de code et que vous avez lancé l’application et réalisé une série de tests manuels pour répliquer le problème, vous pourrez comprendre facilement que l’implémentation d’un test de non-régression avant la correction du bogue vous permettra d’automatiser cette correction de manière efficace.

Pour mettre en œuvre votre test de régression, vous devez suivre la convention d’appellation regression<TicketID> pour vos fonctions de test. Si aucun ticket n’existe pour la régression, vous devez d’abord en créer un. L’utilisation de cette approche permet à la personne qui exécute un test de régression raté d’aller facilement chercher plus d’informations.

//
// 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 ...

Enfin, dans votre déclaration de classe d’essai, vous pouvez déclarer en privé les données dont les membres et les méthodes d’aide dont votre test unitaire peut avoir besoin. Dans notre cas, nous déclarerons une QgsRasterLayer * qui peut être utilisée par n’importe laquelle de nos méthodes d’essai. La couche raster sera créée dans la fonction initTestCase() qui est exécutée avant tout autre test, puis détruite en utilisant cleanupTestCase() qui est exécutée après tous les tests. En déclarant les méthodes d’aide (qui peuvent être appelées par diverses fonctions de test) en privé, vous pouvez vous assurer qu’elles ne seront pas automatiquement exécutées par l’exécutable QTest qui est créé lorsque nous compilons notre test.

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

Ceci termine la déclaration de notre classe. L’implémentation est simplement incluse dans le même fichier plus bas. D’abord la fonction d’initialisation puis la fonction de nettoyage:

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 fonction d’initialisation ci-dessus illustre quelques points d’intérêt.

  1. Nous devions définir manuellement le chemin des données de l’application QGIS afin que des ressources telles que srs.db puissent être trouvées correctement.

  2. Deuxièmement, comme il s’agit d’un test basé sur des données, nous devions fournir un moyen de localiser génériquement le fichier tenbytenraster.asc. Ceci a été réalisé en utilisant le compilateur define TEST_DATA_PATH. La définition est créée dans le fichier de configuration CMakeLists.txt sous <QGIS Source Root>/tests/CMakeLists.txt et est disponible pour tous les tests unitaires QGIS. Si vous avez besoin de données de test pour votre test, validez-le sous <QGIS Source Root>/tests/testdata. Vous ne devriez livrer que de très petits ensembles de données ici. Si votre test a besoin de modifier les données de test, il doit d’abord en faire une copie.

Qt fournit également d’autres mécanismes d’intéressants pour les tests sur les données. Si vous désirez en savoir davantage sur le sujet, consultez la documentation Qt.

Examinons maintenant notre test fonctionnel. Le test isValid() vérifie simplement que la couche raster a été correctement chargée dans le initTestCase. QVERIFY est une macro Qt que vous pouvez utiliser pour évaluer une condition de test. Il existe quelques autres macros d’utilisation que Qt fournit pour vos tests, notamment

  • 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 )

Certaines de ces macros sont utiles uniquement lorsque vous utilisez le cadriciel Qt pour les tests sur les données (consultez la documentation Qt pour plus de détails).

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

Normalement, vos tests fonctionnels devraient couvrir la totalité des fonctionnalités de vos classes publiques d’API, lorsque c’est possible. Maintenant que nos tests fonctionnels sont couverts, nous pouvons nous intéresser à notre exemple de test de non-régression.

Étant donné que le bogue #832 concerne un décompte de cellules incorrect, l’écriture de notre test est simplement une question d’utilisation de QVERIFY pour vérifier que le décompte des cellules correspond bien à la valeur attendue:

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

Avec toutes les fonctions de test unitaire mises en œuvre, il y a une dernière chose que nous devons ajouter à notre classe de test :

QTEST_MAIN(TestQgsRasterLayer)
#include "testqgsrasterlayer.moc"

Le but de ces deux lignes est de signaler à la moc de Qt qu’il s’agit d’un QtTest (il va générer une méthode principale qui à son tour appelle chaque fonction de test. La dernière ligne est l’include pour les sources générées par le MOC. Vous devez remplacer testqgsrasterlayer par le nom de votre classe en minuscules.

6.3. Comparer des images pour tests de rendu

Les images rendues sur des environnements différents peuvent produire de légères différences du aux implémentations spécifiques aux plateformes (ex : rendus de polices de caractères et algorithmes antialiasing différents), du aux polices disponibles sur le système et du à d’autres raisons obscures.

Lorsqu’un test de rendu est effectué sur Travis et qu’il échoue, cherchez le lien du tiret tout en bas du journal de Travis. Ce lien vous mènera à une page de tableau de bord où vous pourrez voir les images rendues par rapport aux images attendues, ainsi qu’une image « différence » qui met en évidence en rouge les pixels qui ne correspondent pas à l’image de référence.

Le système de test unitaire QGIS permet d’ajouter des images de « mask », qui sont utilisées pour indiquer quand une image rendue peut être différente de l’image de référence. Une image mask est une image (portant le même nom que l’image de référence, mais comprenant le suffixe _mask.png), et doit avoir les mêmes dimensions que l’image de référence. Dans une image masque, les valeurs des pixels indiquent dans quelle mesure ce pixel individuel peut différer de l’image de référence. Un pixel noir indique donc que le pixel de l’image rendue doit correspondre exactement au même pixel de l’image de référence. Un pixel avec RVB 2, 2, 2 signifie que l’image rendue peut varier jusqu’à 2 de ses valeurs RVB par rapport à l’image de référence, et un pixel entièrement blanc (255, 255, 255) signifie que le pixel est effectivement ignoré lors de la comparaison entre l’image attendue et l’image rendue.

Un script utilitaire pour générer des images de mask est disponible sous le nom de scripts/generate_test_mask_image.py. Ce script est utilisé en lui passant le chemin d’une image de référence (par exemple tests/testdata/control_images/annotations/expected_annotation_fillstyle/expected_annotation_fillstyle.png) et le chemin de votre image rendue.

Par exemple

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

Vous pouvez raccourcir le chemin du fichier vers l’image de référence en faisant passé à la place une partie du nom de test, ex :

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

(Ce raccourci fonctionne seulement si une seule image de référence correspondante est trouvée. Si plusieurs correspondances sont trouvées vous aurez besoin de fournir le chemin complet vers l’image de référence.)

Le script accepte également les urls http pour l’image rendue, vous pouvez donc copier directement une url d’image rendue depuis la page de résultats cdash et la passer au script.

Faites attention lorsque vous générez des images de mask - vous devez toujours visualiser l’image de mask générée et examiner les zones blanches de l’image. Comme ces pixels sont ignorés, assurez-vous que ces images blanches ne couvrent aucune partie importante de l’image de référence - sinon votre test unitaire n’aura aucun sens !

De même, vous pouvez « blanchir » manuellement des parties du masque si vous voulez délibérément les exclure du test. Cela peut être utile, par exemple, pour les tests qui mélangent le rendu de symboles et de texte (comme les tests de légende), lorsque le test unitaire n’est pas conçu pour tester le texte rendu et que vous ne voulez pas que le test soit soumis à des différences de rendu de texte entre les plates-formes.

Pour comparer les images dans les tests unitaires QGIS, vous devez utiliser la classe « QgsMultiRenderChecker » ou une de ses sous-classes.

Pour améliorer la robustesse des tests, voici quelques conseils :

  1. Désactivez l’anticrénelage si vous le pouvez, car cela minimise les différences de rendu entre les plates-formes.

  2. Assurez-vous que vos images de référence sont « grossières »… c’est-à-dire qu’elles n’ont pas de lignes de 1 px de large ou d’autres caractéristiques fines, et utilisez de grandes polices de caractères gras (14 points ou plus est recommandé).

  3. Parfois, les tests génèrent des images de taille légèrement différente (par exemple, les tests de rendu de légende, où la taille de l’image dépend de la taille de rendu de la police - qui est sujette à des différences entre les plates-formes). Pour tenir compte de cela, utilisez « QgsMultiRenderChecker::setSizeTolerance()`` et spécifiez le nombre maximum de pixels pour lesquels la largeur et la hauteur de l’image rendue diffèrent de l’image de référence.

  4. N’utilisez pas de fonds transparents dans les images de référence (CDash ne les prend pas en charge). Utilisez plutôt QgsMultiRenderChecker::drawBackground() pour dessiner un motif en damier pour le fond de l’image de référence.

  5. Lorsque des polices sont requises, utilisez la police spécifiée dans « QgsFontUtils::standardTestFontFamily()`` (« QGIS Vera Sans »).

Si travis signale des erreurs pour de nouvelles images (par exemple en raison de l’anticrénelage ou de différences de polices), le script parse_dash_results.py peut vous aider lorsque vous mettez à jour les masques de test locaux.

6.4. Ajouter votre test unitaire à CMakeLists.txt

Pour ajouter votre test unitaire au système de compilation, il suffit de modifier le fichier CMakeLists.txt dans le répertoire test, de cloner un des blocs de test existants, puis de remplacer le nom de votre classe de test dans celui-ci. Par exemple :

# QgsRasterLayer test
ADD_QGIS_TEST(rasterlayertest testqgsrasterlayer.cpp)

6.4.1. La macro ADD_QGIS_TEST expliquée

Nous allons parcourir ces lignes brièvement pour expliquer ce qu’elles font, mais si vous n’êtes pas intéressé, faites simplement l’étape expliquée dans la section ci-dessus.

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 Mac OS X, 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)

Examinons un peu plus en détail les différentes lignes. Nous commençons par définir la liste des sources pour notre test. Comme nous n’avons qu’un seul fichier source (suivant la méthodologie décrite ci-dessus où la déclaration de classe et la définition sont dans le même fichier), il s’agit d’une simple déclaration :

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

Etant donné que notre classe doit être lancée à travers le compilateur de méta-objet Qt (cmo), nous devons fournir quelques lignes en plus pour déclencher ce comportement:

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

Ensuite, nous disons à cmake qu’il doit faire un exécutable de la classe de test. Rappelez-vous que dans la section précédente sur la dernière ligne de l’implémentation de la classe, nous avons inclus les sorties de la moc directement dans notre classe de test, ce qui lui donnera (entre autres choses) une méthode principale pour que la classe puisse être compilée comme un exécutable :

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

Ensuite, nous devons préciser les éventuelles dépendances de la bibliothèque. Pour l’instant, les classes ont été implémentées avec une dépendance QT_LIBRARIES fourre-tout, mais nous allons travailler à la remplacer par les bibliothèques Qt spécifiques dont chaque classe a besoin. Bien entendu, vous devez également établir des liens vers les bibliothèques qgis pertinentes, comme l’exige votre test unitaire.

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

Ensuite, nous disons à cmake d’installer les tests au même endroit que les binaires qgis eux-mêmes. C’est une chose que nous prévoyons de supprimer à l’avenir afin que les tests puissent être exécutés directement à partir de l’arbre des sources.

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 Mac OS X, 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)

Enfin, ce qui précède utilise ADD_TEST pour enregistrer le test avec cmake / ctest. C’est ici que la meilleure magie se produit - nous enregistrons la classe avec ctest. Si vous vous souvenez de l’aperçu que nous avons donné au début de cette section, nous utilisons à la fois QtTest et CTest. Pour résumer, QtTest ajoute une méthode principale à votre unité de test et gère l’appel de vos méthodes de test au sein de la classe. Il fournit également quelques macros comme QVERIFY que vous pouvez utiliser pour tester l’échec des tests en utilisant des conditions. La sortie d’un test unitaire QtTest est un exécutable que vous pouvez exécuter en ligne de commande. Cependant, lorsque vous avez une suite de tests et que vous voulez exécuter chaque exécutable à tour de rôle, et mieux encore intégrer l’exécution des tests dans le processus de compilation, c’est le CTest que nous utilisons.

6.5. Compiler votre test unitaire

Pour compiler notre test unitaire, vous devez vous assurer que ENABLE_TESTS=true est dans la configuration de CMake. Il existe deux moyens pour y parvenir:

  1. Lancez cmake .. (ou cmakesetup .. sous MS-Windows) et positionnez interactivement l’option ENABLE_TESTS à ON.

  2. Ajoutez une option à la ligne de commande de cmake; ex: cmake -DENABLE_TESTS=true ..

À part cela, compilez QGIS comme d’habitude et les tests devraient également se compiler.

6.6. Lancer vos tests

Le moyen le plus simple de lancer les tests est de les inclure directement dans le processus de compilation:

make && make install && make test

Enfin, ce qui précède utilise ADD_TEST pour enregistrer le test avec cmake / ctest. Voici où la meilleure magie se produit La commande make test va invoquer CTest qui va exécuter chaque test qui a été enregistré en utilisant la directive CMake ADD_TEST décrite ci-dessus. La sortie typique de make test ressemblera à ceci :

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

Si un test échoue, vous pouvez utiliser la commande ctest pour examiner plus en détails pourquoi il a échoué. Utilisez l’option -R pour indiquer une expression rationnelle pour désigner les tests que vous voulez lancer et -V pour activer la sortie verbeuse.

$ 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. Réalisation de tests individuels

Les tests C++ sont des applications ordinaires. Vous pouvez les exécuter à partir du dossier de construction comme n’importe quel exécutable.

$ ./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 *********

Ces tests prennent également des arguments de ligne de commande. Cela permet d’exécuter un sous-ensemble spécifique de tests :

$ ./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. Tests de l’unité de débogage

Test C++

Pour les tests unitaires C++, QtCreator ajoute automatiquement des cibles d’exécution, de sorte que vous pouvez les lancer depuis le débogueur.

Si vous allez dans Projets et là dans l’onglet Construction et exécution –> Bureau Exécution, vous pouvez également spécifier des paramètres de ligne de commande qui permettront d’exécuter un sous-ensemble de tests dans un fichier .cpp dans le débogueur.

Test Python

Il est également possible de lancer des tests unitaires Python depuis QtCreator avec GDB. Pour cela, vous devez aller dans Projets et choisir Run sous Build & Run. Ensuite, ajoutez une nouvelle configuration Run avec l’exécutable /usr/bin/python3 et les arguments de la ligne de commande placés sur le chemin du fichier python de test unitaire, par exemple /home/user/dev/qgis/QGIS/tests/src/python/test_qgsattributeformeditorwidget.py.

Maintenant, modifiez également l”Environnement d'exécution et ajoutez 3 nouvelles variables :

Variable

Valeur

PYTHONPATH

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

QGIS_PREFIX_PATH

[build]/output

LD_LIBRARY_PATH

[build]/output/lib

Remplacez [build] par votre répertoire de compilation et [source] par le répertoire source.

6.6.3. Amusez-vous

Voilà qui conclut cette section sur l’écriture des tests unitaires dans les QGIS. Nous espérons que vous prendrez l’habitude d’écrire des tests pour tester de nouvelles fonctionnalités et pour vérifier les régressions. Certains aspects du système de test (en particulier les parties CMakeLists.txt) sont encore en cours d’élaboration afin que le cadre de test fonctionne d’une manière réellement indépendante de la plate-forme.