5. Testen van eenheden

Vanaf november 2007 vereisen we dat alle nieuwe mogelijkheden die in master gaan worden vergezeld door een test van eenheden. Initieel hadden we deze eis beperkt tot qgis_core, en we zullen deze eis uitbreiden naar andere delen van de codebasis als mensen eenmaal bekend zijn met de procedures voor het testen van eenheden, uitgelegd in de gedeelten die volgen.

5.1. Het test framework voor QGIS - een overzicht

Testen van eenheden wordt uitgevoerd met behulp van een combinatie van QTestLib (de testbibliotheek van Qt) en CTest (een framework voor het compileren en uitvoeren van testen als deel van het CMake buildproces). Laten we eens naar een overzicht van het proces kijken, voordat we dieper op de details ingaan:

  1. Er is enige code die u wilt testen, bijv. een klasse of functie. Extreme experts van programmeren stellen voor dat de code nog niet zou moeten zijn geschreven wanneer u begint met het bouwen van uw tests, en dan, als u uw code implementeert, kunt u onmiddellijk elk nieuw functionele gedeelte dat u toevoegt kunnen valideren met uw test. In de praktijk dient u waarschijnlijk testen te schrijven voor reeds bestaande code in QGIS omdat we met een test framework beginnen ruim nadat veel logica voor de toepassing al is geïmplementeerd.

  2. U maakt een test voor eenheden. Dit gebeurt onder <QGIS Source Dir>/tests/src/core in het geval van de core lib. De test is in de basis een cliënt die een instantie van ene klasse maakt en enkele methoden van die klasse aanroept. Het zal de teruggave voor elke methoden controleren om er voor te zorgen dat het overeenkomt met de verwachte waarde. Als één van de aanroepen mislukt, zal de eenheid mislukken.

  3. U neemt macro’s van QtTestLib op in uw testklasse. Deze macro wordt verwerkt door de Qt meta object compiler (moc) en breidt uw testklasse uit naar een uitvoerbare toepassing.

  4. U voegt een gedeelte toe aan CMakeLists.txt in uw map met testen die uw test zal bouwen.

  5. Zorg er voor dat u ENABLE_TESTING heeft ingeschakeld in ccmake / cmakesetup. Dat zal er voor zorgen dat uw testen in feite worden gecompileerd als u make typt.

  6. U voegt optioneel testgegevens toe aan <QGIS Source Dir>/tests/testdata als uw test aangedreven wordt door gegevens (bijv. dient een shapefile te laden). Deze testgegevens zouden zo klein mogelijk moeten zijn en waar mogelijk zou u de reeds daar aanwezige gegevens moeten gebruiken. Uw testen zouden nooit die gegevens in situ moeten aanpassen, maar in plaats daarvan ergens een tijdelijke kopie moeten maken, indien nodig.

  7. U compileert uw bronnen en installeert. Doe dit met behulp van de normale procedure make && (sudo)  make install.

  8. U voert uw testen uit. Dit wordt normaal gesproken eenvoudig gedaan door make test te doen na de stap make install, hoewel we wel andere benaderingen uitleggen die meer fijnmazige controle over het uitvoeren van testen bieden.

Met dat overzicht in gedachten zullen we een beetje meer ingaan op de details. We hebben al veel van de configuratie voor u gedaan in CMake en op andere plaatsen in de boom van de bron, dus alles wat u nog zou moeten doen zijn de eenvoudige gedeelten - testen voor eenheden schrijven!

5.2. Een test voor eenheden maken

Het maken van een test voor eenheden is eenvoudig - gewoonlijk zult u dit doen door één enkel .cpp-bestand te maken (er wordt geen .h-bestand gebruikt) en implementeer al uw testmethoden als publieke methoden die void teruggeven. We gebruiken, ter illustratie, een eenvoudige testklasse voor QgsRasterLayer in het gedeelte dat hierop volgt. Volgens conventie zullen we onze test dezelfde naam geven als de klasse die getest wordt, maar met het voorvoegsel ‘Test’. Dus onze test-implementatie gaat in een bestand genaamd testqgsrasterlayer.cpp en de klasse zelf zal zijn TestQgsRasterLayer. Eerst voegen we onze standaard banner voor auteursrechten toe:

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

Vervolgens beginnen we met het vermelden van onze includes die nodig zijn voor de testen die we willen uitvoeren. Er is één speciale include die alle testen zouden moeten hebben:

#include <QtTest/QtTest>

Naast dat u gewoon doorgaat met het implementeren van uw klasse zoals gewoonlijk, er headers in opnemend die u nodig zou kunnen hebben:

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

Omdat we zowel de declaratie van de klasse als de implementatie in één enkel bestand hebben, is het volgende de declaratie van de klasse. We beginnen met onze documentatie voor doxygen. Elke test zou juist gedocumenteerd moeten zijn. We gebruiken het doxygen ingroup directive zodat alle UnitTests als een module verschijnen in de gegenereerde documentatie voor Doxygen. Daarna komt ene korte beschrijving van de test van de eenheid en de klasse moet erven van QObject en de macro Q_OBJECT bevatten.

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

class TestQgsRasterLayer: public QObject
{
    Q_OBJECT

Alle onze testmethoden worden geïmplementeerd als private slots. Het framework QtTest zal op volgorde vervolgens elke methode private slot in de testklasse aanroepen. Er zijn vier ‘speciale’ methoden die, indien geïmplementeerd, zullen worden aangeroepen aan het begin van de test van de eenheid (initTestCase), aan het einde van de test van de eenheid (cleanupTestCase). Vóórdat elke testmethode wordt aangeroepen, wordt de methode init() aangeroepen en na elke testmethode wordt de methode cleanup() aangeroepen. Deze methoden zij handig omdat zij u in staat stellen bronnen toe te wijzen en op te schonen, voorafgaande aan elke test, en de test van de eenheid als geheel.

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

Dan komen uw testmethoden, die geen van allen parameters zouden moeten hebben en void zouden moeten teruggeven. De methoden zullen worden aangeroepen in de volgorde van hun declaratie. We implementeren hier twee methoden die twee typen van testen illustreren.

In het eerste geval willen we in het algemeen testen of de verschillende delen van de klasse werken. We kunnen een functionele benadering voor het testen gebruiken. Nogmaals, zeer ervaren programmeurs zouden er misschien voor pleiten deze testen te implementeren vóór het implementeren van de klasse. Als u zich dan door uw implementatie van de klasse heen werkt, voert u achtereenvolgens uw testen van de eenheid uit. Meer en meer testfuncties zouden met succes moeten voltooien als het werk aan uw implementatie van de klasse vordert, en wanneer de gehele test van de eenheid slaagt, is uw nieuwe klasse klaar en is nu compleet met een te herhalen manier om hem te valideren.

Gewoonlijk zouden uw testen voor eenheden alleen de public API van uw klasse behandelen, en normaal gesproken behoeft u geen testen te schrijven voor accessors en mutators. Als het zou gebeuren dat een acccessor of mutator niet werkt zoals u verwacht, zou u normaal gesproken een regressietest implementeren om hierop te controleren.

//
// Functional Testing
//

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

// more functional tests here ...

5.2.1. Een regressietest implementeren

Vervolgens implementeren we onze regressietesten. Regressietesten zouden moeten worden geïmplementeerd om de voorwaarden van een bepaald probleem te kunnen reproduceren. Bijvoorbeeld:

  1. We ontvingen per e-mail een rapport dat de telling voor cellen in een er steeds 1, naast zat, wat alle statistieken voor de rasterbanden onbetrouwbaar maakte.

  2. We openden een rapport voor het probleem (ticket #832)

  3. We maakten een regressietest die het probleem reproduceerde met behulp van een kleine test-gegevensset (een raster van 10x10).

  4. We voerden de test uit en verifieerden inderdaad dat hij faalde (de telling van de cellen kwam uit op 99 in plaats van op 100).

  5. Hierna repareerden we het probleem en voerden de eenheidstest opnieuw uit en de regressietest werd met succes uitgevoerd. We dienden de regressietest samen in met de oplossing van het probleem. Als nu iemand in de toekomst de broncode opnieuw breekt, kunnen we onmiddellijk identificeren dat de code achteruit is gegaan.

    Beter nog, voordat wijzigingen in de toekomst worden ingediend, zou het uitvoeren van onze testen er voor zorgen dat dat onze wijzigingen geen onverwachte neveneffecten hebben - zoals het breken van bestaande functionaliteit.

Er is nog een voordeel van regressietests - zij kunnen u tijd besparen. Als u ooit een bug oploste die mede bestond uit het maken van wijzigingen aan de bron, en daarna de toepassing uitvoerde en een reeks gecompliceerde stappen uitvoerde om het probleem te repliceren, zal het onmiddellijk duidelijk zijn dat het eenvoudigweg implementeren van uw regressietest vóór het oplossen van het probleem het automatiseren van het testen voor oplossingen voor het probleem op een efficiënte manier laat uitvoeren.

U zou, voor het implementeren van uw regressietest, de conventie voor namen van regressie<TicketID> moeten volgen voor uw testfuncties. Als er geen ticket bestaat voor de regressie zou u er eerst een moeten maken. Het gebruiken van deze benadering stelt de persoon die een mislukte regressietest uitvoerde in staat eenvoudig door te gaan en meer informatie te zoeken.

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

Tenslotte kunt u in uw declaratie van de testklasse nog persoonlijk enkele gegevensleden en hulpmethoden declareren die uw test voor eenheid nodig zou kunnen hebben. In ons geval zal ik een QgsRasterLayer * declareren die kan worden gebruikt door elk van onze testmethoden. De rasterlaag zal worden gemaakt in de functie initTestCase() die wordt uitgevoerd vóór enige andere test, en dan worden vernietigd met behulp van cleanupTestCase() die na elke test wordt uitgevoerd. Door het persoonlijk declareren van hulpmethoden (die aangeroepen kunnen worden door verscheidene testfuncties), kunt u er voor zorgen dat zij niet automatisch zullen worden uitgevoerd door de uitvoerbare QTest die wordt gemaakt wanneer we onze test compileren.

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

Dat beëindigt onze declaratie van de klasse. De implementatie is eenvoudigweg opgenomen in hetzelfde bestand hieronder. Eerst onze functies init en 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;
}

Bovenstaande functie init illustreert een aantal interessante dingen.

  1. We moesten handmatig het gegevenspad naar de toepassing van QGIS instellen zodat bronnen zoals srs.db op de juiste manier worden gevonden.

  2. Ten tweede is dat een door gegevens gedreven test dus moesten we een manier verschaffen om generiek het bestand tenbytenraster.asc te lokaliseren. Dit werd bereikt door met behulp van de compiler het TEST_DATA_PATH te definiëren. De definitie wordt gemaakt in het configuratiebestand CMakeLists.txt onder <QGIS Source Root>/tests/CMakeLists.txt en is beschikbaar voor alle testen van eenheden in QGIS. Als u gegevens voor uw test dient te testen, plaats het dan onder <QGIS Source Root>/tests/testdata. U zou hier slechts hele kleine gegevenssets moeten plaatsen. Als uw test de testgegevens dient aan te passen zou het daar eerst een kopie van moeten maken.

Qt verschaft ook enige andere interessante mechanismen voor gegevens gedreven testen, als u dus meer wilt weten over dit onderwerp, consulteer dan de documentatie van Qt.

Laten we vervolgens eens kijken naar onze functionele test. De test isValid() controleert eenvoudigweg of de rasterlaag juist werd geladen in de initTestCase. QVERIFY is een macro van Qt die u kunt gebruiken om de de voorwaarde van de test te evalueren. er zijn ook nog ene paar andere macro’s die Qt verschaft om te gebruiken bij uw testen, inclusief:

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

Enkele van deze macro’s zijn alleen nuttig bij het gebruiken van het Qt framework voor gegevens gedreven testen (bekijk de documentatie van Qt voor meer details).

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

Normaal gesproken zouden uw functionele testen het gehele bereik van de functionaliteit behandelen van uw klassen voor de public API waar mogelijk. met onze functionele testen uit de weg kunnen we kijken naar het voorbeeld van onze regressietest.

Omdat het probleem in bug #832 een rapport is over een foutieve telling van cellen, is het schrijven van onze test eenvoudigweg een geval van QVERIFY gebruiken om te controleren of de telling van de cellen voldoet aan de verwachte waarde:

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

Met alle functies voor de test van de eenheid geïmplementeerd, is er één laatste ding dat we moeten doen om onze testklasse toe te voegen:

QTEST_MAIN(TestQgsRasterLayer)
#include "testqgsrasterlayer.moc"

Het doel van deze twee regels is om een signaal af te geven naar Qt’s moc dat dit een QtTest is (het zal een hoofdmethode genereren die op zijn beurt elke testfunctie aanroept. De laatste regel is de include voor de door MOC gegenereerde bronnen. U zou testqgsrasterlayer moeten vervangen door de naam van uw klasse in kleine letters.

5.3. Afbeeldingen vergelijken voor testen van renderen

Renderen van afbeeldingen op verschillende omgevingen kan subtiele verschillen produceren wegens platform-specifieke implementaties (bijv. verschillend renderen van lettertypen en algoritmen voor antialiasing), voor de beschikbare lettertypen op het systeem en om andere onduidelijke redenen.

Wanneer een test voor renderen wordt uitgevoerd op Travis en mislukt, zoek dan naar de link met het streepje aan de uiterste onderzijde van het log van Travis. Deze link zal u meenemen naar een pagina van cdash, waar u de gerenderde vs verwachte afbeeldingen kunt zien, naast een afbeeldingen met “verschillen” die in rood pixels accentueert die niet overeen kwamen met de afbeelding waarnaar verwezen werd.

Het systeem voor het testen van eenheden van QGIS heeft ondersteuning voor het toevoegen van “masker”afbeeldingen, die worden gebruikt om aan te geven wanneer een gerenderde afbeelding af zou kunnen wijken van de afbeelding waarnaar verwezen wordt. Een maskerafbeelding is een afbeelding (met dezelfde naam en als de afbeelding waarnaar verwezen wordt, maar inclusief het achtervoegsel _mask.png), en zou van dezelfde dimensies moeten zijn als de afbeelding waarnaar verwezen wordt. In een maskerafbeelding geven de pixelwaarden aan hoeveel die individuele pixel mag afwijken van de afbeelding waarnaar verwezen wordt, dus een zwarte pixel geeft aan dat de pixel in de gerenderde afbeelding moet exact overeenkomen met dezelfde pixel in de afbeelding waarnaar verwezen wordt. Een pixel met RGB 2, 2, 2 betekent dat de gerenderde afbeelding tot maximaal 2 kan variëren in zijn waarden voor RGB in de afbeelding waarnaar verwezen wordt, en een volledig witte pixel (255, 255, 255) betekent dat de pixel effectief wordt genegeerd bij het vergelijken van de verwachte en gerenderde afbeeldingen.

Een utility-script om maskerafbeeldingen te genereren is beschikbaar als scripts/generate_test_mask_image.py. Dit script wordt gebruikt door het pad van een afbeelding waarnaar verwezen moet worden (bijv. tests/testdata/control_images/annotations/expected_annotation_fillstyle/expected_annotation_fillstyle.png) en het pad naar uw gerenderde afbeelding door te geven.

Bijv.

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

U kunt het pad naar de afbeelding waarnaar verwezen moet worden verkorten door in plaats daarvan een gedeelte van de testnaam door te geven, bijv.

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

(Dit verkorten werkt alleen als er één enkele overeenkomende afbeelding, waarnaar verwezen moet worden, wordt gevonden. Als meerdere overeenkomsten worden gevonden dient u het volledige pad naar de afbeelding waarnaar verwezen moet worden op te geven.)

Het script accepteert ook URL’s voor HTTP voor de gerenderde afbeelding. U kunt dus een URL van een gerenderde afbeelding direct vanuit de pagina met resultaten van cdash kopiëren en in het script plakken.

Wees voorzichtig bij het maken van maskerafbeeldingen - u zou altijd de maskerafbeelding moeten bekijken en witte gebieden in de afbeeldingen nader moeten bekijken. Omdat die pixels worden genegeerd, zorg er dan voor dat die witte afbeeldingen geen belangrijke gedeelten van de afbeelding waarnaar verwezen wordt bedekken – anders heeft uw eenheidstest geen betekenis!

Op dezelfde wijze kunt u handmatig gedeelten van het masker “wit maken” als u ze met opzet wilt uitsluiten van de test. Dit kan nuttig zijn voor bijv. testen met een mix van symbool- en tekstrenderen (zoals testen van de legenda), waar de eenheidstest niet is ontworpen om de gerenderde tekst te testen en u niet wilt dat de test onderworpen wordt aan kruis-platform verschillen bij het renderen van tekst.

U zou de klasse QgsMultiRenderChecker of een van zijn subklassen moeten gebruiken om afbeeldingen te vergelijken met eenheidstesten van QGIS.

Hier zijn enkele tips om de robuustheid van testen te verbeteren:

  1. Schakel antialiasing uit als u kunt, dit minimaliseert kruis-platform verschillen bij renderen.

  2. Zorg er voor dat afbeeldingen waarnaar verwezen wordt “blokkig”… zijn, d. i. geen lijnen met een breedte van 1 px hebben of andere fijne objecten, en gebruik grote, vette lettertypen (14 punten of meer wordt aanbevolen).

  3. Soms maken testen enigszins afwijkende grootten van afbeeldingen (bijv. testen voor renderen van legenda’s, waar de grootte van de afbeelding afhankelijk is van de grootte voor renderen van het lettertype - wat onderwerp is voor kruis-platform verschillen). Gebruik, om hier rekening mee te houden, QgsMultiRenderChecker::setSizeTolerance() en specificeer het maximale aantal pixels die de gerenderde afbeelding in breedte en hoogte mag afwijken van de afbeelding waarnaar verwezen wordt.

  4. Gebruik geen transparante achtergronden in afbeelding waarnaar verwezen wordt (CDash ondersteunt ze niet). Gebruik in plaats daarvan QgsMultiRenderChecker::drawBackground() om een patroon voor een schaakbord te tekenen voor de achtergrond van de afbeelding waarnaar verwezen wordt.

  5. Wanneer lettertypen vereist zijn, gebruik dan het lettertype dat is gespecificeerd in QgsFontUtils::standardTestFontFamily() (“QGIS Vera Sans”).

A;s Travis fouten rapporteert voor nieuwe afbeeldingen (bijvoorbeeld vanwege anti-aliasing of verschillen in lettertypen), kan het script parse_dash_results.py u helpen als u de lokale testmaskers bijwerkt.

5.4. Uw test voor eenheden toevoegen aan CMakeLists.txt

Toevoegen van uw test voor eenheden aan het bouwsysteem is eenvoudigweg een geval van het bewerken van CMakeLists.txt in de map test, klonen van één van de bestaande tekstblokken, en dan de naam van uw testklasse daar invullen. Bijvoorbeeld:

# QgsRasterLayer test
ADD_QGIS_TEST(rasterlayertest testqgsrasterlayer.cpp)

5.4.1. De macro ADD_QGIS_TEST uitgelegd

We zullen kort door deze regels gaan om uit te leggen wat zij doen, maar als u daar niet in geïnteresseerd bent, doe dan alleen de stap die is uitgelegd in bovenstaand gedeelte.

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)

Laten we eens iets meer in detail kijken naar de individuele regels. Eerst definiëren we de lijst met bronnen voor onze test. Omdat we slechts één bronbestand hebben (aldus de methodologie volgend die hierboven is beschreven waar declaratie van de klasse en definitie in hetzelfde bestand staan) is het een eenvoudig argument:

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

Omdat onze testklasse moet worden uitgevoerd door de Qt meta object compiler (moc) dienen we ook een aantal regels te verschaffen om dat mogelijk te maken:

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

Vervolgens vertellen we cmake dat het een executable moet maken vanuit de testklasse. Onthoud dat in het vorige gedeelte op de laatste regel van de implementatie van de klasse we de uitvoer voor MOC direct opnamen in onze testklasse, zodat het (naast ander dingen) een hoofdmethode zal geven zodat de klasse als een executable kan worden gecompileerd:

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

Vervolgens dienen we enkele afhankelijkheden voor bibliotheken te specificeren. Op dit moment worden klassen geïmplementeerd met een catch-all QT_LIBRARIES afhankelijkheid, maar we zullen er aan werken om dat te vervangen door de specifieke bibliotheken van Qt die elke klasse alleen nodig heeft. Natuurlijk dient u ook de relevante bibliotheken van QGIS te koppelen, zoals vereist door uw test voor eenheden.

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

Vervolgens vertellen we cmake om de testen te installeren op dezelfde plaats als waar de binaries van QGIS zelf staan. Dit is iets waarvan ik van plan ben om dit in de toekomst te verwijderen zodat de testen direct vanuit de boom van de bron kunnen worden uitgevoerd.

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)

Tenslotte gebruikt bovenstaande ADD_TEST om de test te registreren met cmake / ctest. Hier is waar de beste magie gebeurt - we registreren de klasse met ctest. Als u zich het overzicht nog herinnert dat we gaven in het begin van dit gedeelte, gebruiken we zowel QtTest als CTest samen. Recapitulerend, QtTest voegt een hoofdmethode toe aan uw test voor eenheden en behandelt het aanroepen van uw testmethoden binnen de klasse. Het verschaft ook enkele macro’s zoals QVERIFY die u kunt gebruiken om te testen op mislukkingen van de door de testen gebruikte voorwaarden. De uitvoer van een QtTest test voor eenheden is een executable die u kunt uitvoeren vanaf de opdrachtregel. Wanneer u echter een suite van testen heeft en u wilt elke executable op zijn beurt uitvoeren, en beter nog uitvoerende testen integreren in het bouwproces, is de CTest wat we gebruiken.

5.5. Uw test voor eenheden bouwen

Voor het bouwen van de test voor eenheden dient u er alleen voor te zorgen dat ENABLE_TESTS=true in de configuratie cmake. Er zijn twee manieren om dat te doen:

  1. Voer ccmake .. uit ( of cmakesetup .. onder Windows) en stel interactief de vlag ENABLE_TESTS in op ON.

  2. Voeg een vlag voor de opdrachtregel toe aan cmake bijv. cmake -DENABLE_TESTS=true ..

Anders dan dat, bouw QGIS gewoon zoals normaal en de testen zouden meegebouwd moeten worden.

5.6. Voer uw testen uit

De eenvoudigste manier om de testen uit te voeren is als deel van uw normale bouwproces:

make && make install && make test

De opdracht make test zal CTest activeren dat elke test zal uitvoeren die werd geregistreerd met behulp van de ADD_TEST CMake directive beschreven hierboven. Normale uitvoer van make test zal er uitzien zoals dit:

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

Als een test mislukt, kunt u de opdracht ctest gebruiken om meer nader te bekijken waarom het mislukt is. Gebruik de optie -R om een regex te specificeren voor de testen die u wilt uitvoeren en -V om uitgebreide uitvoer te krijgen:

$ 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

5.6.1. Individuele testen uitvoeren

C++-testen zijn gewone toepassingen. U kunt ze uitvoeren vanuit de map build, net als elk ander uitvoerbaar bestand.

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

Deze testen accepteren ook argumenten voor de opdrachtregel. Dit maakt het mogelijk een specifieke subset van testen uit te voeren:

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

5.6.2. Debuggen met eenheidstesten

5.6.2.1. C++-testen

Voor eenheidstesten in C++ voegt QtCreator automatisch doelen voor uitvoering toe, dus u kunt ze vanuit de debugger starten.

Als u gaat naar Projects en daar dan naar Build & Run –> Desktop tab Run, kunt u ook parameters voor de opdrachtregel specificeren die toestaan dat een subset van de testen worden uitgevoerd binnen een bestand .cpp in de debugger.

5.6.2.2. Python-testen

Het is ook mogelijk eenheidstesten voor Python te starten vanuit QtCreator met GDB. Hiervoor dient u te gaan naar Projects en te kiezen voor Run onder Build & Run. Voeg dan een nieuwe Run configuration toe met de executable /usr/bin/python3 en de argumenten voor de opdrachtregel ingesteld op het pad van het bestand voor Python van de eenheidstest, bijv. /home/user/dev/qgis/QGIS/tests/src/python/test_qgsattributeformeditorwidget.py.

Wijzig nu ook de Run Environment en voeg 3 nieuwe variabelen toe:

Variabele

Waarde

PYTHONPATH

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

QGIS_PREFIX_PATH

[build]/output

LD_LIBRARY_PATH

[build]/output/lib

Vervang [build] door de map van uw build en [source] door de map van uw bron.

5.6.3. Veel plezier

Welnu, dat is het einde voor dit gedeelte over het schrijven van testen voor eenheden in QGIS. We hopen dat u ook testen zult gaan schrijven om nieuwe functionaliteiten te testen en om te controleren op regressies. Aan sommige aspecten van het testsysteem (in het bijzonder de delen voor CMakeLists.txt) wordt nog steeds gewerkt zodat het framework voor testen werkt op een echte platform-onafhankelijke manier.