Prueba de Unidad

A noviembre de 2007, requerimos todas las nuevas características que van a sobreponerse a ser acompañados con una unidad de prueba. Al principio, nos hemos limitado a este requisito qgis_core, y vamos a ampliar este a otras partes de la base de código una vez que la gente está familiarizada con los procedimientos para las pruebas unitarias que se explican en las secciones que siguen.

La infraestructura de prueba de QGIS - una visión general

La prueba de unidad se lleva a cabo utilizando una combinación de QTestLib (la biblioteca de pruebas Qt) y CTest (una infraestructura para compilar y ejecutar pruebas como parte del proceso de construcción de CMake). Demos una vista general del proceso antes de ahondar en los detalles:

  • Hay algo de código que desea probar, por ejemplo, una clase o función. defensores de programación extremas sugieren que el código no debe ser escrito incluso aún cuando se inicia la construcción de sus pruebas, y luego como implementar su código que pueden validar de inmediato cada nueva pieza funcional se agrega con su prueba. En la práctica, es probable que necesite escribir pruebas para el código preexistente en QGIS ya estamos empezando con un marco de pruebas bien después de la lógica de aplicación mucho ya se ha aplicado.
  • Se crea una prueba de unidad. Esto pasa bajo <QGIS Source Dir>/tests/src/core en caso de la biblioteca core. La prueba es básicamente un cliente que crea una instancia de la clase y llama algunos métodos de la clase. Revisará la respuesta de cada una para asegurarse de que coincide con el valor esperado. Si cualquiera de las llamadas falla, la unidad no funciona.
  • Se incluyen macros QtTestLib en su clase de prueba. Este macro es procesado por el comilador objeto meta Qt (moc) y expande su clase de prueba en una aplicación ejecutable.
  • Se agrega una sección a la CMakeLists.txt en el directorio de pruebas que construirá su prueba.
  • Asegurarse que tiene habilitado ENABLE_TESTING en ccmake / cmakesetup. Esto asegurará que sus pruebas en realidad se compilen cuando se teclea make.
  • Se añaden opcionalmente datos de prueba a <QGIS Source Dir>/tests/testdata si su prueba utiliza la información (por ejemplo, necesita cargar un archivo shape). Estos datos de prueba deben ser lo más pequeñas posible y siempre que sea posible se debe utilizar las bases de datos existentes ya allí. Sus pruebas nunca deben modificar estos datos in situ, sino más bien debe hacer una copia temporal en algún lugar si es necesario.
  • Se compila sus fuentes e instalar. Para ello, utilice el procedimiento normal make && (sudo) make install.
  • Ejecutar las pruebas. Normalmente, esto se hace simplemente haciendo make test después del paso make install, aunque voy a explicar otros enfoques que ofrecen un control de grano más fino sobre las pruebas de funcionamiento.

Derecho con ese panorama en mente, voy a ahondar un poco de detalle. Ya se ha hecho la mayor parte de la configuración para usted en CMake y otros lugares en el árbol de origen por lo que todo lo que necesita hacer son los bits fáciles - ¡escribir las pruebas unitarias!

Creando una prueba de unidad

Crear una unidad de prueba es fácil - por lo general, esto se hace sólo por la creación de un único archivo .cpp (no se utiliza el archivo .h) y poner en práctica todos los métodos de prueba y públicos que devuelven void. Voy a usar una clase de prueba simple para QgsRasterLayer en toda la sección que sigue para ilustrar. Por convenio nombraremos nuestra prueba con el mismo nombre que la clase que están poniendo a prueba, pero con el prefijo “Test”. Así que la implementación de prueba va en un archivo llamado qgsrasterlayer.cpp de prueba y la propia clase será prueba QgsRasterLayer. Primero añadimos nuestro banner estándar de copyright:

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

Next we use start our includes needed for the tests we plan to run. There is one special include all tests should have:

#include <QtTest/QtTest>

Además de que acaba de continuar la implementación de su clase con normalidad, tirando en cualquier cabeceras que pueda necesitar:

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

Since we are combining both class declaration and implementation in a single file the class declaration comes next. We start with our doxygen documentation. Every test case should be properly documented. We use the doxygen ingroup directive so that all the UnitTests appear as a module in the generated Doxygen documentation. After that comes a short description of the unit test and the class must inherit from QObject and include the Q_OBJECT macro.

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

class TestQgsRasterLayer: public QObject
{
    Q_OBJECT

All our test methods are implemented as private slots. The QtTest framework will sequentially call each private slot method in the test class. There are four “special” methods which if implemented will be called at the start of the unit test (initTestCase), at the end of the unit test (cleanupTestCase). Before each test method is called, the init() method will be called and after each test method is called the cleanup() method is called. These methods are handy in that they allow you to allocate and cleanup resources prior to running each test, and the test unit as a whole.

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

Then come your test methods, all of which should take no parameters and should return void. The methods will be called in order of declaration. I am implementing two methods here which illustrates two types of testing. In the first case I want to generally test the various parts of the class are working, I can use a functional testing approach. Once again, extreme programmers would advocate writing these tests before implementing the class. Then as you work your way through your class implementation you iteratively run your unit tests. More and more test functions should complete sucessfully as your class implementation work progresses, and when the whole unit test passes, your new class is done and is now complete with a repeatable way to validate it.

Typically your unit tests would only cover the public API of your class, and normally you do not need to write tests for accessors and mutators. If it should happen that an acccessor or mutator is not working as expected you would normally implement a regression test to check for this (see lower down).

//
// Functional Testing
//

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

// more functional tests here ...

Next we implement our regression tests. Regression tests should be implemented to replicate the conditions of a particular bug. For example I recently received a report by email that the cell count by rasters was off by 1, throwing off all the statistics for the raster bands. I opened a bug (ticket #832) and then created a regression test that replicated the bug using a small test dataset (a 10x10 raster). Then I ran the test and ran it, verifying that it did indeed fail (the cell count was 99 instead of 100). Then I went to fix the bug and reran the unit test and the regression test passed. I committed the regression test along with the bug fix. Now if anybody breakes this in the source code again in the future, we can immediatly identify that the code has regressed. Better yet before committing any changes in the future, running our tests will ensure our changes don’t have unexpected side effects - like breaking existing functionality.

There is one more benefit to regression tests - they can save you time. If you ever fixed a bug that involved making changes to the source, and then running the application and performing a series of convoluted steps to replicate the issue, it will be immediately apparent that simply implementing your regression test before fixing the bug will let you automate the testing for bug resolution in an efficient manner.

To implement your regression test, you should follow the naming convention of regression<TicketID> for your test functions. If no redmine ticket exists for the regression, you should create one first. Using this approach allows the person running a failed regression test easily go and find out more information.

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

Finally in our test class declaration you can declare privately any data members and helper methods your unit test may need. In our case I will declare a QgsRasterLayer * which can be used by any of our test methods. The raster layer will be created in the initTestCase() function which is run before any other tests, and then destroyed using cleanupTestCase() which is run after all tests. By declaring helper methods (which may be called by various test functions) privately, you can ensure that they wont be automatically run by the QTest executable that is created when we compile our test.

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

Con esto termina nuestra declaración de la clase. La aplicación es simplemente inline en el mismo archivo más abajo. Primero nuestro init y funciones de limpieza:

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 función init anterior ilustra un par de cosas interesantes.

  1. Se necesita establecer manualmente la ruta de datos de aplicación de QGIS para que los recursos tales como srs.db se puedan encontrar correctamente.
  2. Secondly, this is a data driven test so we needed to provide a way to generically locate the tenbytenraster.asc file. This was achieved by using the compiler define TEST_DATA_PATH. The define is created in the CMakeLists.txt configuration file under <QGIS Source Root>/tests/CMakeLists.txt and is available to all QGIS unit tests. If you need test data for your test, commit it under <QGIS Source Root>/tests/testdata. You should only commit very small datasets here. If your test needs to modify the test data, it should make a copy of it first.

Qt también algunos otros mecanismos interesantes para los datos de las pruebas conducidas, por lo que si está interesado en saber más sobre el tema, consulte la documentación de Qt.

A continuación veamos nuestra prueba funcional. La prueba isValid() simplemente comprueba que la capa ráster ha cargado correctamente en el initTestCase. QVERIFY es una macro Qt que se puede utilizar para evaluar una condición de prueba. Hay algunas otros macros de uso que Qt proporcionan para su uso en sus pruebas, incluyendo:

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

Algunos de estos macros son útiles solo al utilizar el infraestructura Qt para datos dados de prueba (vea la documentación de Qt para mayor información).

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

Normalmente las pruebas funcionales cubrirían toda la gama de funcionalidad de sus clases de la API pública siempre que sea posible. Con nuestras pruebas funcionales fuera, podemos mirar a nuestro ejemplo de pruebas de regresión.

Dado que el problema en el error #832 es un informe erroneo de conteo de celdas, escribiendo nuestra prueba es simplemente es cuestión de usar QVERIFY para comprobar que el recuento de celdas se encuentra con el valor esperado:

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 todas las funciones de la unidad de prueba implementados, hay una última cosa que necesitamos para añadir a nuestra clase de prueba:

QTEST_MAIN(TestQgsRasterLayer)
#include "testqgsrasterlayer.moc"

El propósito de estas dos líneas es señalarle al moc de Qt que la suya es una QtTest (que va a generar un método principal que a su vez llama a cada función de prueba .La última línea es la de incluir el MOC que genera fuentes. Debe reemplazar “testqgsrasterlayer” con el nombre de su clase en minúsculas.

Añadir su prueba de unidad a CMakeLists.txt

Añadir su unidad de prueba al sistema de construcción es simplemente una cuestión de la edición de la CMakeLists.txt en el directorio de prueba, la clonación de uno de los bloques de prueba existentes, y volviendo a poner el nombre de su clase de prueba en ella. Por ejemplo:

# QgsRasterLayer test
ADD_QGIS_TEST(rasterlayertest testqgsrasterlayer.cpp)

La macro ADD_QGIS_TEST explicado

Se correr a través de estas líneas para explicar brevemente lo que hacen, pero si usted no está interesado, sólo realice el paso explicado en la sección anterior y esta sección.

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)

Veamos un poco más en detalle en las líneas individuales. En primer lugar definimos la lista de fuentes para nuestra prueba. Ya que tenemos un único archivo de origen (siguiendo la metodología que he descrito anteriormente, donde se declaró la clase y la definición están en el mismo archivo) es una declaración simple:

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

Desde nuestra clase de prueba se debe ejecutar a través del compilador objeto meta Qt (moc) que necesitamos para proporcionar un par de líneas para que esto suceda también:

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

A continuación le decimos a cmake que debe hacer un ejecutable a partir de la clase de prueba. Recuerde que en la sección anterior sobre la última línea de la implementación de la clase se incluyeron las salidas moc directamente en nuestra clase de prueba, por lo que le dará (entre otras cosas) un método principal por tanto la clase se puede compilar como un archivo ejecutable:

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

A continuación tenemos que especificar las dependencias de la bibliotecas. Por el momento, las clases han sido implementadas con un catch-all dependencia QT_LIBRARIES, pero se trabaja para sustituir eso con las librerías Qt específicas que cada clase necesita solamente. Por supuesto también es necesario para enlazar a las bibliotecas qgis pertinentes según lo requiera la prueba de la unidad.

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

A continuación le digo a cmake para instalar las pruebas para el mismo lugar que los qgis binarios. Esto es algo que va a extraer en el futuro para que las pruebas se puedan ejecutar directamente desde el interior del árbol de origen.

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)

Finalmente los usos anteriores ADD_TEST para registrar la prueba con cmake / ctest. Aquí es donde la mejor magia sucede - que registra la clase con ctest. Si recuerdan en el resumen que se dio al comienzo de esta sección, se esta utilizando tanto QtTest y CTest juntos. Para recapitular, QtTest añade un método principal para su unidad de prueba y se encarga de llamar a sus métodos de prueba dentro de la clase. También proporciona algunas macros como QVERIFY que se puede utilizar como para probar la insuficiencia de las pruebas utilizando condiciones. El resultado de una prueba de unidad QtTest es un archivo ejecutable que se puede ejecutar desde la línea de comandos. Sin embargo cuando se tiene un conjunto de pruebas y se desea ejecutar cada ejecutable a su vez, y mejor aún integrar las pruebas que se ejecutan en el proceso de construcción, el CTest es lo que usamos.

Creando su unidad de prueba

Para construir la unidad de prueba sólo tendrá que asegurarse de que ENABLE_TESTS= true en la configuración cmake. Hay dos maneras de hacer esto:

  1. Ejecutar ccmake .. ( o cmakesetup .. bajo windows) e interactivamente establecer la marca ENABLE_TESTS a ON.
  2. Añadir una marca de línea de comandos para cmake por ejemplo cmake -DENABLE_TESTS=true ..

Aparte de eso, simplemente construir QGIS con normalidad y las pruebas deben crearse también.

Ejecutar las pruebas

La forma más sencilla de ejecutar las pruebas es como parte de su proceso de construcción normal:

make && make install && make test

El comando de prueba make invocará CTest que se ejecutará en cada prueba que se registró usando la directiva ADD_TEST que CMake ha descrito anteriormente. La salida típica de la prueba de make se verá así:

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 una prueba falla, se puede utilizar el comando ctest para examinar más de cerca por qué ha fallado. Utilice la opción -R para especificar una expresión regular para las pruebas que desea ejecutar y -V para obtener una salida detallada:

$ 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

Así concluye esta sección sobre escribir las pruebas unitarias en QGIS. Esperamos que obtenga el hábito de la prueba de escritura para probar nuevas funcionalidades y para comprobar si hay regresiones. Algunos aspectos del sistema de prueba (en particular, las partes CMakeLists.txt) todavía se está trabajando para que el marco de pruebas funcione de una verdadera plataforma. Se actualizará este documento como progresan las cosas.