mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-14 00:07:35 -04:00
Completed my tutorial on unit testing as I promised I would at the last QGIS developer meeting...
git-svn-id: http://svn.osgeo.org/qgis/trunk@7656 c8812cc2-4d05-0410-92ff-de0c093fc19c
This commit is contained in:
parent
ccfed84ec5
commit
c39a9b743f
587
CODING
587
CODING
@ -46,7 +46,13 @@
|
||||
2.6.5. Due Diligence
|
||||
2.7. Obtaining SVN Write Access
|
||||
2.7.1. Procedure once you have access
|
||||
3. Authors
|
||||
3. Unit Testing
|
||||
3.1. The QGIS testing framework - an overview
|
||||
3.2. Creating a unit test
|
||||
3.3. Adding your unit test to CMakeLists.txt
|
||||
3.4. Building your unit test
|
||||
3.5. Run your tests
|
||||
4. Authors
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
@ -78,7 +84,7 @@ Class in QGIS begin with Qgs and are formed using mixed case.
|
||||
1.1.2. Members
|
||||
==============
|
||||
|
||||
Class member names begin with a lower case ''m'' and are formed using mixed case.
|
||||
Class member names begin with a lower case m and are formed using mixed case.
|
||||
|
||||
|
||||
mMapCanvas
|
||||
@ -86,13 +92,13 @@ Class member names begin with a lower case ''m'' and are formed using mixed case
|
||||
|
||||
|
||||
All class members should be private.
|
||||
'''Public class members are STRONGLY discouraged'''
|
||||
Public class members are STRONGLY discouraged
|
||||
|
||||
|
||||
1.1.3. Accessor Functions
|
||||
=========================
|
||||
|
||||
Class member values should be obtained through accesssor functions. The function should be named without a ''get'' prefix. Accessor functions for the two private members above would be:
|
||||
Class member values should be obtained through accesssor functions. The function should be named without a get prefix. Accessor functions for the two private members above would be:
|
||||
|
||||
|
||||
mapCanvas()
|
||||
@ -118,7 +124,7 @@ Function names begin with a lowercase letter and are formed using mixed case. Th
|
||||
1.2.1. Generated Classes
|
||||
========================
|
||||
|
||||
QGIS classes that are generated from Qt Designer (ui) files should have a ''Base'' suffix. This identifies the class as a generated base class.
|
||||
QGIS classes that are generated from Qt Designer (ui) files should have a Base suffix. This identifies the class as a generated base class.
|
||||
|
||||
|
||||
Examples:
|
||||
@ -131,8 +137,8 @@ QGIS classes that are generated from Qt Designer (ui) files should have a ''Base
|
||||
|
||||
All dialogs should implement the following:
|
||||
* Tooltip help for all toolbar icons and other relevant widgets
|
||||
* WhatsThis help for '''all''' widgets on the dialog
|
||||
* An optional (though highly recommended) context sensitive ''Help'' button that directs the user to the appropriate help page by launching their web browser
|
||||
* WhatsThis help for all widgets on the dialog
|
||||
* An optional (though highly recommended) context sensitive Help button that directs the user to the appropriate help page by launching their web browser
|
||||
|
||||
|
||||
1.3. C++ Files
|
||||
@ -343,10 +349,10 @@ So, prefer this:
|
||||
1.6.6. Book recommendations
|
||||
===========================
|
||||
|
||||
* http://www.awprofessional.com/title/0321334876 Effective (C++), Scott Meyers
|
||||
* http://www.awprofessional.com/bookstore/product.asp?isbn=020163371X&rl=1 More Effective (C++), Scott Meyers
|
||||
* http://www.awprofessional.com/title/0201749629 Effective (STL), Scott Meyers
|
||||
* http://www.awprofessional.com/title/0201634988 Design (Patterns), GoF
|
||||
* Effective C++ (http://www.awprofessional.com/title/0321334876), Scott Meyers
|
||||
* More Effective C++ (http://www.awprofessional.com/bookstore/product.asp?isbn=020163371X&rl=1), Scott Meyers
|
||||
* Effective STL (http://www.awprofessional.com/title/0201749629), Scott Meyers
|
||||
* Design Patterns (http://www.awprofessional.com/title/0201634988), GoF
|
||||
|
||||
|
||||
2. SVN Access
|
||||
@ -386,10 +392,10 @@ To check out SVN stable trunk:
|
||||
svn co https://svn.qgis.org/repos/qgis/trunk/qgis qgis_unstable
|
||||
|
||||
|
||||
/!\ '''Note:''' If you are behind a proxy server, edit your ~/subversion/servers file to specify
|
||||
/!\ Note: If you are behind a proxy server, edit your ~/subversion/servers file to specify
|
||||
your proxy settings first!
|
||||
|
||||
/!\ '''Note:''' In QGIS we keep our most stable code in trunk. Periodically we will tag a release
|
||||
/!\ Note: In QGIS we keep our most stable code in trunk. Periodically we will tag a release
|
||||
off trunk, and then continue stabilisation and selective incorporation of new features into trunk.
|
||||
|
||||
See the INSTALL file in the source tree for specific instructions on building development versions.
|
||||
@ -514,7 +520,7 @@ deal with the patches that are sent to use easily.
|
||||
========================
|
||||
|
||||
If the patch is a fix for a specific bug, please name the file with the bug number in it e.g.
|
||||
'''bug777fix.diff''', and attach it to the original bug report in trac (https://svn.qgis.org/trac).
|
||||
bug777fix.diff, and attach it to the original bug report in trac (https://svn.qgis.org/trac).
|
||||
|
||||
If the bug is an enhancement or new feature, its usually a good idea to create a ticket in
|
||||
trac (https://svn.qgis.org/trac) first and then attach you
|
||||
@ -633,7 +639,558 @@ Save and close in your editor. The first time you do this, you should be prompte
|
||||
put in your username and password. Just use the same ones as your trac account.
|
||||
|
||||
|
||||
3. Authors
|
||||
3. Unit Testing
|
||||
===============
|
||||
|
||||
As of November 2007 we require all new features going into trunk to be accompanied with
|
||||
a unit test. Initially we have limited this requirement to qgis_core, and we will extend
|
||||
this requirement to other parts of the code base once people are familiar with the
|
||||
procedures for unit testing explained in the sections that follow.
|
||||
|
||||
|
||||
3.1. The QGIS testing framework - an overview
|
||||
==============================================
|
||||
|
||||
Unit testing is carried out using a combination of QTestLib (the Qt testing library) and
|
||||
CTest (a framework for compiling and running tests as part of the CMake build process).
|
||||
Lets take an overview of the process before I delve into the details:
|
||||
|
||||
* There is some code you want to test, e.g. a class or function. Extreme programming
|
||||
advocates suggest that the code should not even be written yet when you start
|
||||
building your tests, and then as you implement your code you can immediately validate
|
||||
each new functional part you add with your test. In practive you will probably
|
||||
need to write tests for pre-existing code in QGIS since we are starting with a testing
|
||||
framework well after much application logic has already been implemented.
|
||||
|
||||
* You create a unit test. This happens under <QGIS Source Dir>/tests/src/core
|
||||
in the case of the core lib. The test is basically a client that creates an instance
|
||||
of a class and calls some methods on that class. It will check the return from each
|
||||
method to make sure it matches the expected value. If any one of the calls fails,
|
||||
the unit will fail.
|
||||
|
||||
* You include QtTestLib macros in your test class. This macro is processed by
|
||||
the Qt meta object compiler (moc) and expands your test class into a runnable application.
|
||||
|
||||
* You add a section to the CMakeLists.txt in your tests directory that will
|
||||
build your test.
|
||||
|
||||
* You ensure you have ENABLE_TESTING enabled in ccmake / cmakesetup. This
|
||||
will ensure your tests actually get compiled when you type make.
|
||||
|
||||
* You optionally add test data to <QGIS Source Dir>/tests/testdata if your
|
||||
test is data driven (e.g. needs to load a shapefile). These test data should be
|
||||
as small as possible and wherever possible you should use the existing datasets
|
||||
already there. Your tests should never modify this data in situ, but rather
|
||||
may a temporary copy somewhere if needed.
|
||||
|
||||
* You compile your sources and install. Do this using normal make && (sudo)
|
||||
make install procedure.
|
||||
|
||||
* You run your tests. This is normally done simply by doing make test
|
||||
after the make install step, though I will explain other aproaches that offer more
|
||||
fine grained control over running tests.
|
||||
|
||||
Right with that overview in mind, I will delve into a bit of detail. I've already
|
||||
done much of the configuration for you in CMake and other places in the source tree
|
||||
so all you need to do are the easy bits - writing unit tests!
|
||||
|
||||
|
||||
3.2. Creating a unit test
|
||||
=========================
|
||||
|
||||
Creating a unit test is easy - typically you will do this by just creating a
|
||||
single .cpp file (not .h file is used) and implement all your test methods as
|
||||
public methods that return void. I'll use a simple test class for QgsRasterLayer
|
||||
throughout the section that follows to illustrate. By convention we will name our
|
||||
test with the same name as the class they are testing but prefixed with 'Test'.
|
||||
So our test implementation goes in a file called testqgsrasterlayer.cpp and
|
||||
the class itself will be TestQgsRasterLayer. First we add our standard copyright
|
||||
banner:
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
testqgsvectorfilewriter.cpp
|
||||
--------------------------------------
|
||||
Date : Frida Nov 23 2007
|
||||
Copyright : (C) 2007 by Tim Sutton
|
||||
Email : tim@linfiniti.com
|
||||
***************************************************************************
|
||||
* *
|
||||
* 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>
|
||||
|
||||
|
||||
Beyond that you just continue implementing your class as per normal, pulling
|
||||
in whatever headers you may need:
|
||||
|
||||
|
||||
//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:
|
||||
|
||||
|
||||
/** \ingroup UnitTests
|
||||
* This is a unit test for the QgsRasterLayer class.
|
||||
*/
|
||||
|
||||
|
||||
The class must inherit from QObject and include the Q_OBJECT macro.
|
||||
|
||||
|
||||
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 to 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 dont have unexpected side effects - like breaking
|
||||
existing functionality.
|
||||
|
||||
There is one more benifit 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
|
||||
trac 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 executeable 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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
That ends our class declaration. The implementation is simply
|
||||
inlined in the same file lower down. First our init and cleanup functions:
|
||||
|
||||
|
||||
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 << "Prefix PATH: " << QgsApplication::prefixPath().toLocal8Bit().data() << std::endl;
|
||||
std::cout << "Plugin PATH: " << 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
The above init function illustrates a couple of interesting things.
|
||||
|
||||
1. I needed to manually set the QGIS application data path so that
|
||||
resources such as srs.db can be found properly.
|
||||
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 if first.
|
||||
|
||||
Qt also provides some other interesting mechanisms for data driven
|
||||
testing, so if you are interested to know more on the topic, consult
|
||||
the Qt documentation.
|
||||
|
||||
Next lets look at our functional test. The isValid() test simply
|
||||
checks the raster layer was correctly loaded in the initTestCase.
|
||||
QVERIFY is a Qt macro that you can use to evaluate a test condition.
|
||||
There are a few other use macros Qt provide for use in your tests
|
||||
including:
|
||||
|
||||
|
||||
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 )
|
||||
|
||||
|
||||
Some of these macros are useful only when using the Qt framework
|
||||
for data driven testing (see the Qt docs for more detail).
|
||||
|
||||
|
||||
void TestQgsRasterLayer::isValid()
|
||||
{
|
||||
QVERIFY ( mpLayer->isValid() );
|
||||
}
|
||||
|
||||
|
||||
Normally your functional tests would cover all the range of
|
||||
functionality of your classes public API where feasible. With our
|
||||
functional tests out the way, we can look at our regression test example.
|
||||
|
||||
Since the issue in bug #832 is a misreported cell count, writing
|
||||
our test if simply a matter of using QVERIFY to check that the
|
||||
cell count meets the expected value:
|
||||
|
||||
|
||||
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 );
|
||||
}
|
||||
|
||||
|
||||
With all the unit test functions implemented, there one final thing we
|
||||
need to add to our test class:
|
||||
|
||||
|
||||
QTEST_MAIN(TestQgsRasterLayer)
|
||||
#include "moc_testqgsrasterlayer.cxx"
|
||||
|
||||
|
||||
The purpose of these two lines is to signal to Qt's moc that his is a
|
||||
QtTest (it will generate a main method that in turn calls each test funtion.
|
||||
The last line is the include for the MOC generated sources. You should
|
||||
replace 'testqgsrasterlayer' with the name of your class in lower case.
|
||||
|
||||
|
||||
3.3. Adding your unit test to CMakeLists.txt
|
||||
============================================
|
||||
|
||||
Adding your unit test to the build system is simply a matter of editing
|
||||
the CMakeLists.txt in the test directory, cloning one of the existing
|
||||
test blocks, and then search and replacing your test class name into it.
|
||||
For example:
|
||||
|
||||
|
||||
#
|
||||
# QgsRasterLayer test
|
||||
#
|
||||
SET(qgis_rasterlayertest_SRCS testqgsrasterlayer.cpp)
|
||||
SET(qgis_rasterlayertest_MOC_CPPS testqgsrasterlayer.cpp)
|
||||
QT4_WRAP_CPP(qgis_rasterlayertest_MOC_SRCS ${qgis_rasterlayertest_MOC_CPPS})
|
||||
ADD_CUSTOM_TARGET(qgis_rasterlayertestmoc ALL DEPENDS ${qgis_rasterlayertest_MOC_SRCS})
|
||||
ADD_EXECUTABLE(qgis_rasterlayertest ${qgis_rasterlayertest_SRCS})
|
||||
ADD_DEPENDENCIES(qgis_rasterlayertest qgis_rasterlayertestmoc)
|
||||
TARGET_LINK_LIBRARIES(qgis_rasterlayertest ${QT_LIBRARIES} qgis_core)
|
||||
INSTALL(TARGETS qgis_rasterlayertest RUNTIME DESTINATION ${QGIS_BIN_DIR})
|
||||
ADD_TEST(qgis_rasterlayertest ${QGIS_BIN_DIR}/qgis_rasterlayertest)
|
||||
|
||||
|
||||
I'll run through these lines briefly to explain what they do, but if
|
||||
you are not interested, just clone the block, search and replace e.g.
|
||||
|
||||
|
||||
:'<,'>s/rasterlayer/mynewtest/g
|
||||
|
||||
|
||||
Lets look a little more in detail at the individual lines. First we
|
||||
define the list of sources for our test. Since we have only one source file
|
||||
(following the methodology I described above where class declaration and
|
||||
definition are in the same file) its a simple statement:
|
||||
|
||||
|
||||
SET(qgis_rasterlayertest_SRCS testqgsrasterlayer.cpp)
|
||||
|
||||
|
||||
Since our test class needs to be run through the Qt meta object compiler (moc)
|
||||
we need to provide a couple of lines to make that happen too:
|
||||
|
||||
|
||||
SET(qgis_rasterlayertest_MOC_CPPS testqgsrasterlayer.cpp)
|
||||
QT4_WRAP_CPP(qgis_rasterlayertest_MOC_SRCS ${qgis_rasterlayertest_MOC_CPPS})
|
||||
ADD_CUSTOM_TARGET(qgis_rasterlayertestmoc ALL DEPENDS ${qgis_rasterlayertest_MOC_SRCS})
|
||||
|
||||
|
||||
Next we tell cmake that it must make an executeable from the test class.
|
||||
Remember in the previous section on the last line of the class implementation
|
||||
I included the moc outputs directly into our test class, so that will
|
||||
give it (among other things) a main method so the class can be
|
||||
compiled as an executeable:
|
||||
|
||||
|
||||
ADD_EXECUTABLE(qgis_rasterlayertest ${qgis_rasterlayertest_SRCS})
|
||||
ADD_DEPENDENCIES(qgis_rasterlayertest qgis_rasterlayertestmoc)
|
||||
|
||||
|
||||
Next we need to specify any library dependencies. At the moment classes
|
||||
have been implemented with a catch-all QT_LIBRARIES dependency, but I will
|
||||
be working to replace that with the specific Qt libraries that each class
|
||||
needs only. Of course you also need to link to the relevant qgis
|
||||
libraries as required by your unit test.
|
||||
|
||||
|
||||
TARGET_LINK_LIBRARIES(qgis_rasterlayertest ${QT_LIBRARIES} qgis_core)
|
||||
|
||||
|
||||
Next I tell cmake to the same place as the qgis binaries itself. This
|
||||
is something I plan to remove in the future so that the tests can
|
||||
run directly from inside the source tree.
|
||||
|
||||
|
||||
INSTALL(TARGETS qgis_rasterlayertest RUNTIME DESTINATION ${QGIS_BIN_DIR})
|
||||
|
||||
|
||||
Finally here is where the best magic happens - we register the class with
|
||||
ctest. If you recall in the overview I gave in the beginning of this
|
||||
section we are using both QtTest and CTest together. To recap, QtTest adds a
|
||||
main method to your test unit and handles calling your test methods within
|
||||
the class. It also provides some macros like QVERIFY that you can use as
|
||||
to test for failure of the tests using conditions. The output from
|
||||
a QtTest unit test is an executeable which you can run from the command line.
|
||||
However when you have a suite of tests and you want to run each executeable
|
||||
in turn, and better yet integrate running tests into the build process,
|
||||
the CTest is what we use. The next line registers the unit test with
|
||||
CMake / CTest.
|
||||
|
||||
|
||||
ADD_TEST(qgis_rasterlayertest ${QGIS_BIN_DIR}/qgis_rasterlayertest)
|
||||
|
||||
|
||||
The last thing I should add is that if your test requires optional
|
||||
parts of the build process (e.g. Postgresql support, GSL libs, GRASS etc.),
|
||||
you should take care to enclose you test block inside a IF () block
|
||||
in the CMakeLists.txt file.
|
||||
|
||||
|
||||
3.4. Building your unit test
|
||||
============================
|
||||
|
||||
To build the unit test you need only to make sure that ENABLE_TESTS=true
|
||||
in the cmake configuration. There are two ways to do this:
|
||||
|
||||
1. Run ccmake .. (cmakesetup .. under windows) and interactively set
|
||||
the ENABLE_TESTS flag to ON.
|
||||
1. Add a command line flag to cmake e.g. cmake -DENABLE_TESTS=true ..
|
||||
|
||||
Other than that, just build QGIS as per normal and the tests should build
|
||||
too.
|
||||
|
||||
|
||||
3.5. Run your tests
|
||||
===================
|
||||
|
||||
The simplest way to run the tests is as part of your normal build process:
|
||||
|
||||
|
||||
make && make install && make test
|
||||
|
||||
|
||||
The make test command will invoke CTest which will run each test that
|
||||
was registered using the ADD_TEST CMake directive described above. Typical
|
||||
output from make test will look like this:
|
||||
|
||||
|
||||
Running tests...
|
||||
Start processing tests
|
||||
Test project /Users/tim/dev/cpp/qgis/build
|
||||
1/ 3 Testing qgis_applicationtest ***Exception: Other
|
||||
2/ 3 Testing qgis_filewritertest *** Passed
|
||||
3/ 3 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
|
||||
|
||||
|
||||
If a test fails, you can use the ctest command to examine more
|
||||
closely why it failed. User the -R option to specify a regex for
|
||||
which tests you want to run and -V to get verbose output:
|
||||
|
||||
|
||||
[build] 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
|
||||
1/ 3 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()
|
||||
Prefix PATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/../
|
||||
Plugin PATH: /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()
|
||||
Prefix PATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/../
|
||||
Plugin PATH: /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
|
||||
|
||||
|
||||
|
||||
Well that concludes this section on writing unit tests in QGIS. We hope you
|
||||
will get into the habit of writing test to test new functionality and to
|
||||
check for regressions. Some aspects of the test system (in particular the
|
||||
CMakeLists.txt parts) are still being worked on so that the testing framework
|
||||
works in a truly platform way. I will update this document as things progress.
|
||||
|
||||
|
||||
4. Authors
|
||||
==========
|
||||
|
||||
* Tim Sutton (author and editor)
|
||||
|
567
CODING.t2t
567
CODING.t2t
@ -1,23 +1,23 @@
|
||||
%!encoding: iso-8859-1
|
||||
|
||||
|
||||
% These are comments and will not be generated in any output
|
||||
% -------------------
|
||||
|
||||
%This document is in text2tags format. You can generate html, plain text and
|
||||
%moinmoin formatted documentation by running txt2tags on this document. See the
|
||||
%txt2tags home page for more details. Please insert manual line breaks in this
|
||||
%document as it makes diffing for changes much easier. To do this in vim
|
||||
%automatically, select a section then issue (gq) command. Please dont
|
||||
%apply vim formatting to the whole document as it screws up some formatting
|
||||
%rather apply it selectively to paragraphs where needed.
|
||||
|
||||
% To generate the text version of this document:
|
||||
% txt2tags -t txt --toc --enum-title -o CODING CODING.t2t
|
||||
% To generate the moinmoin version of this document
|
||||
% txt2tags -t moin --toc --enum-title -o CODING.moin CODING.t2t
|
||||
|
||||
% End of comments
|
||||
%!encoding: iso-8859-1
|
||||
|
||||
|
||||
% These are comments and will not be generated in any output
|
||||
% -------------------
|
||||
|
||||
%This document is in text2tags format. You can generate html, plain text and
|
||||
%moinmoin formatted documentation by running txt2tags on this document. See the
|
||||
%txt2tags home page for more details. Please insert manual line breaks in this
|
||||
%document as it makes diffing for changes much easier. To do this in vim
|
||||
%automatically, select a section then issue (gq) command. Please dont
|
||||
%apply vim formatting to the whole document as it screws up some formatting
|
||||
%rather apply it selectively to paragraphs where needed.
|
||||
|
||||
% To generate the text version of this document:
|
||||
% txt2tags -t txt --toc --enum-title -o CODING CODING.t2t
|
||||
% To generate the moinmoin version of this document
|
||||
% txt2tags -t moin --toc --enum-title -o CODING.moin CODING.t2t
|
||||
|
||||
% End of comments
|
||||
% -------------------
|
||||
|
||||
|
||||
@ -25,9 +25,9 @@
|
||||
% Insert the following preamble on moinmoin generated output
|
||||
%-----------------------------------------------------------------
|
||||
|
||||
%/!\ '''Note:''' Please do not edit this document directly.
|
||||
%/!\ **Note:** Please do not edit this document directly.
|
||||
%
|
||||
%/!\ '''Note:''' Please do not remove this notice.
|
||||
%/!\ **Note:** Please do not remove this notice.
|
||||
%
|
||||
%(!) This document was generated using text2tags from INSTALL.t2t in the QGIS sources. Make your
|
||||
% edits to that file and use t2t to regenerate in moinmoin %format, then paste the procedure in below.
|
||||
@ -58,16 +58,16 @@ Examples:
|
||||
```
|
||||
|
||||
=== Members ===
|
||||
Class member names begin with a lower case ''m'' and are formed using mixed case.
|
||||
Class member names begin with a lower case //m// and are formed using mixed case.
|
||||
```
|
||||
mMapCanvas
|
||||
mCurrentExtent
|
||||
```
|
||||
|
||||
All class members should be private.
|
||||
'''Public class members are STRONGLY discouraged'''
|
||||
**Public class members are STRONGLY discouraged**
|
||||
=== Accessor Functions ===
|
||||
Class member values should be obtained through accesssor functions. The function should be named without a ''get'' prefix. Accessor functions for the two private members above would be:
|
||||
Class member values should be obtained through accesssor functions. The function should be named without a //get// prefix. Accessor functions for the two private members above would be:
|
||||
```
|
||||
mapCanvas()
|
||||
currentExtent()
|
||||
@ -82,7 +82,7 @@ Function names begin with a lowercase letter and are formed using mixed case. Th
|
||||
|
||||
== Qt Designer ==
|
||||
=== Generated Classes ===
|
||||
QGIS classes that are generated from Qt Designer (ui) files should have a ''Base'' suffix. This identifies the class as a generated base class.
|
||||
QGIS classes that are generated from Qt Designer (ui) files should have a //Base// suffix. This identifies the class as a generated base class.
|
||||
```
|
||||
Examples:
|
||||
QgsPluginMangerBase
|
||||
@ -91,8 +91,8 @@ Examples:
|
||||
=== Dialogs ===
|
||||
All dialogs should implement the following:
|
||||
* Tooltip help for all toolbar icons and other relevant widgets
|
||||
* WhatsThis help for '''all''' widgets on the dialog
|
||||
* An optional (though highly recommended) context sensitive ''Help'' button that directs the user to the appropriate help page by launching their web browser
|
||||
* WhatsThis help for **all** widgets on the dialog
|
||||
* An optional (though highly recommended) context sensitive //Help// button that directs the user to the appropriate help page by launching their web browser
|
||||
== C++ Files ==
|
||||
=== Names ===
|
||||
C++ implementation and header files should be have a .cpp and .h extension respectively.
|
||||
@ -261,10 +261,10 @@ So, prefer this:
|
||||
|
||||
=== Book recommendations ===
|
||||
|
||||
* [http://www.awprofessional.com/title/0321334876 Effective C++], Scott Meyers
|
||||
* [http://www.awprofessional.com/bookstore/product.asp?isbn=020163371X&rl=1 More Effective C++], Scott Meyers
|
||||
* [http://www.awprofessional.com/title/0201749629 Effective STL], Scott Meyers
|
||||
* [http://www.awprofessional.com/title/0201634988 Design Patterns], GoF
|
||||
* [Effective C++ http://www.awprofessional.com/title/0321334876], Scott Meyers
|
||||
* [More Effective C++ http://www.awprofessional.com/bookstore/product.asp?isbn=020163371X&rl=1], Scott Meyers
|
||||
* [Effective STL http://www.awprofessional.com/title/0201749629], Scott Meyers
|
||||
* [Design Patterns http://www.awprofessional.com/title/0201634988], GoF
|
||||
|
||||
|
||||
|
||||
@ -298,10 +298,10 @@ To check out SVN stable trunk:
|
||||
svn co https://svn.qgis.org/repos/qgis/trunk/qgis qgis_unstable
|
||||
```
|
||||
|
||||
/!\ '''Note:''' If you are behind a proxy server, edit your ~/subversion/servers file to specify
|
||||
/!\ **Note:** If you are behind a proxy server, edit your ~/subversion/servers file to specify
|
||||
your proxy settings first!
|
||||
|
||||
/!\ '''Note:''' In QGIS we keep our most stable code in trunk. Periodically we will tag a release
|
||||
/!\ **Note:** In QGIS we keep our most stable code in trunk. Periodically we will tag a release
|
||||
off trunk, and then continue stabilisation and selective incorporation of new features into trunk.
|
||||
|
||||
See the INSTALL file in the source tree for specific instructions on building development versions.
|
||||
@ -409,7 +409,7 @@ deal with the patches that are sent to use easily.
|
||||
=== Patch file naming ===
|
||||
|
||||
If the patch is a fix for a specific bug, please name the file with the bug number in it e.g.
|
||||
'''bug777fix.diff''', and attach it to the original bug report in trac (https://svn.qgis.org/trac).
|
||||
**bug777fix.diff**, and attach it to the original bug report in trac (https://svn.qgis.org/trac).
|
||||
|
||||
If the bug is an enhancement or new feature, its usually a good idea to create a ticket in
|
||||
trac (https://svn.qgis.org/trac) first and then attach you
|
||||
@ -536,38 +536,38 @@ Unit testing is carried out using a combination of QTestLib (the Qt testing libr
|
||||
CTest (a framework for compiling and running tests as part of the CMake build process).
|
||||
Lets take an overview of the process before I delve into the details:
|
||||
|
||||
* '''There is some code you want to test''', e.g. a class or function. Extreme programming
|
||||
* **There is some code you want to test**, e.g. a class or function. Extreme programming
|
||||
advocates suggest that the code should not even be written yet when you start
|
||||
building your tests, and then as you implement your code you can immediately validate
|
||||
each new functional part you add with your test. In practive you will probably
|
||||
need to write tests for pre-existing code in QGIS since we are starting with a testing
|
||||
framework well after much application logic has already been implemented.
|
||||
|
||||
* '''You create a unit test.''' This happens under <QGIS Source Dir>/tests/src/core
|
||||
* **You create a unit test.** This happens under <QGIS Source Dir>/tests/src/core
|
||||
in the case of the core lib. The test is basically a client that creates an instance
|
||||
of a class and calls some methods on that class. It will check the return from each
|
||||
method to make sure it matches the expected value. If any one of the calls fails,
|
||||
the unit will fail.
|
||||
|
||||
* '''You include QtTestLib macros in your test class.''' This macro is processed by
|
||||
* **You include QtTestLib macros in your test class.** This macro is processed by
|
||||
the Qt meta object compiler (moc) and expands your test class into a runnable application.
|
||||
|
||||
* '''You add a section to the CMakeLists.txt''' in your tests directory that will
|
||||
* **You add a section to the CMakeLists.txt** in your tests directory that will
|
||||
build your test.
|
||||
|
||||
* '''You ensure you have ENABLE_TESTING enabled in ccmake / cmakesetup.''' This
|
||||
* **You ensure you have ENABLE_TESTING enabled in ccmake / cmakesetup.** This
|
||||
will ensure your tests actually get compiled when you type make.
|
||||
|
||||
* '''You optionally add test data to <QGIS Source Dir>/tests/testdata''' if your
|
||||
* **You optionally add test data to <QGIS Source Dir>/tests/testdata** if your
|
||||
test is data driven (e.g. needs to load a shapefile). These test data should be
|
||||
as small as possible and wherever possible you should use the existing datasets
|
||||
already there. Your tests should never modify this data in situ, but rather
|
||||
may a temporary copy somewhere if needed.
|
||||
|
||||
* '''You compile your sources and install.''' Do this using normal make && (sudo)
|
||||
* **You compile your sources and install.** Do this using normal make && (sudo)
|
||||
make install procedure.
|
||||
|
||||
* '''You run your tests.''' This is normally done simply by doing '''make test'''
|
||||
* **You run your tests.** This is normally done simply by doing **make test**
|
||||
after the make install step, though I will explain other aproaches that offer more
|
||||
fine grained control over running tests.
|
||||
|
||||
@ -577,12 +577,493 @@ so all you need to do are the easy bits - writing unit tests!
|
||||
|
||||
== Creating a unit test ==
|
||||
|
||||
Creating a unit test is easy - typically you will do this by just creating a
|
||||
single .cpp file (not .h file is used) and implement all your test methods as
|
||||
public methods that return void. I'll use a simple test class for QgsRasterLayer
|
||||
throughout the section that follows to illustrate. By convention we will name our
|
||||
test with the same name as the class they are testing but prefixed with 'Test'.
|
||||
So our test implementation goes in a file called testqgsrasterlayer.cpp and
|
||||
the class itself will be TestQgsRasterLayer. First we add our standard copyright
|
||||
banner:
|
||||
|
||||
```
|
||||
/***************************************************************************
|
||||
testqgsvectorfilewriter.cpp
|
||||
--------------------------------------
|
||||
Date : Frida Nov 23 2007
|
||||
Copyright : (C) 2007 by Tim Sutton
|
||||
Email : tim@linfiniti.com
|
||||
***************************************************************************
|
||||
* *
|
||||
* 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>
|
||||
```
|
||||
|
||||
Beyond that you just continue implementing your class as per normal, pulling
|
||||
in whatever headers you may need:
|
||||
|
||||
```
|
||||
//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:
|
||||
|
||||
```
|
||||
/** \ingroup UnitTests
|
||||
* This is a unit test for the QgsRasterLayer class.
|
||||
*/
|
||||
```
|
||||
|
||||
The class **must** inherit from QObject and include the Q_OBJECT macro.
|
||||
|
||||
```
|
||||
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 to 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 dont have unexpected side effects - like breaking
|
||||
existing functionality.
|
||||
|
||||
There is one more benifit 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
|
||||
trac 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 executeable 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;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
That ends our class declaration. The implementation is simply
|
||||
inlined in the same file lower down. First our init and cleanup functions:
|
||||
|
||||
```
|
||||
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 << "Prefix PATH: " << QgsApplication::prefixPath().toLocal8Bit().data() << std::endl;
|
||||
std::cout << "Plugin PATH: " << 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;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The above init function illustrates a couple of interesting things.
|
||||
|
||||
1. I needed to manually set the QGIS application data path so that
|
||||
resources such as srs.db can be found properly.
|
||||
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 if first.
|
||||
|
||||
Qt also provides some other interesting mechanisms for data driven
|
||||
testing, so if you are interested to know more on the topic, consult
|
||||
the Qt documentation.
|
||||
|
||||
Next lets look at our functional test. The isValid() test simply
|
||||
checks the raster layer was correctly loaded in the initTestCase.
|
||||
QVERIFY is a Qt macro that you can use to evaluate a test condition.
|
||||
There are a few other use macros Qt provide for use in your tests
|
||||
including:
|
||||
|
||||
```
|
||||
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 )
|
||||
```
|
||||
|
||||
Some of these macros are useful only when using the Qt framework
|
||||
for data driven testing (see the Qt docs for more detail).
|
||||
|
||||
```
|
||||
void TestQgsRasterLayer::isValid()
|
||||
{
|
||||
QVERIFY ( mpLayer->isValid() );
|
||||
}
|
||||
```
|
||||
|
||||
Normally your functional tests would cover all the range of
|
||||
functionality of your classes public API where feasible. With our
|
||||
functional tests out the way, we can look at our regression test example.
|
||||
|
||||
Since the issue in bug #832 is a misreported cell count, writing
|
||||
our test if simply a matter of using QVERIFY to check that the
|
||||
cell count meets the expected value:
|
||||
|
||||
```
|
||||
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 );
|
||||
}
|
||||
```
|
||||
|
||||
With all the unit test functions implemented, there one final thing we
|
||||
need to add to our test class:
|
||||
|
||||
```
|
||||
QTEST_MAIN(TestQgsRasterLayer)
|
||||
#include "moc_testqgsrasterlayer.cxx"
|
||||
```
|
||||
|
||||
The purpose of these two lines is to signal to Qt's moc that his is a
|
||||
QtTest (it will generate a main method that in turn calls each test funtion.
|
||||
The last line is the include for the MOC generated sources. You should
|
||||
replace 'testqgsrasterlayer' with the name of your class in lower case.
|
||||
|
||||
== Adding your unit test to CMakeLists.txt ==
|
||||
|
||||
Adding your unit test to the build system is simply a matter of editing
|
||||
the CMakeLists.txt in the test directory, cloning one of the existing
|
||||
test blocks, and then search and replacing your test class name into it.
|
||||
For example:
|
||||
|
||||
```
|
||||
#
|
||||
# QgsRasterLayer test
|
||||
#
|
||||
SET(qgis_rasterlayertest_SRCS testqgsrasterlayer.cpp)
|
||||
SET(qgis_rasterlayertest_MOC_CPPS testqgsrasterlayer.cpp)
|
||||
QT4_WRAP_CPP(qgis_rasterlayertest_MOC_SRCS ${qgis_rasterlayertest_MOC_CPPS})
|
||||
ADD_CUSTOM_TARGET(qgis_rasterlayertestmoc ALL DEPENDS ${qgis_rasterlayertest_MOC_SRCS})
|
||||
ADD_EXECUTABLE(qgis_rasterlayertest ${qgis_rasterlayertest_SRCS})
|
||||
ADD_DEPENDENCIES(qgis_rasterlayertest qgis_rasterlayertestmoc)
|
||||
TARGET_LINK_LIBRARIES(qgis_rasterlayertest ${QT_LIBRARIES} qgis_core)
|
||||
INSTALL(TARGETS qgis_rasterlayertest RUNTIME DESTINATION ${QGIS_BIN_DIR})
|
||||
ADD_TEST(qgis_rasterlayertest ${QGIS_BIN_DIR}/qgis_rasterlayertest)
|
||||
```
|
||||
|
||||
I'll run through these lines briefly to explain what they do, but if
|
||||
you are not interested, just clone the block, search and replace e.g.
|
||||
|
||||
```
|
||||
:'<,'>s/rasterlayer/mynewtest/g
|
||||
```
|
||||
|
||||
Lets look a little more in detail at the individual lines. First we
|
||||
define the list of sources for our test. Since we have only one source file
|
||||
(following the methodology I described above where class declaration and
|
||||
definition are in the same file) its a simple statement:
|
||||
|
||||
```
|
||||
SET(qgis_rasterlayertest_SRCS testqgsrasterlayer.cpp)
|
||||
```
|
||||
|
||||
Since our test class needs to be run through the Qt meta object compiler (moc)
|
||||
we need to provide a couple of lines to make that happen too:
|
||||
|
||||
```
|
||||
SET(qgis_rasterlayertest_MOC_CPPS testqgsrasterlayer.cpp)
|
||||
QT4_WRAP_CPP(qgis_rasterlayertest_MOC_SRCS ${qgis_rasterlayertest_MOC_CPPS})
|
||||
ADD_CUSTOM_TARGET(qgis_rasterlayertestmoc ALL DEPENDS ${qgis_rasterlayertest_MOC_SRCS})
|
||||
```
|
||||
|
||||
Next we tell cmake that it must make an executeable from the test class.
|
||||
Remember in the previous section on the last line of the class implementation
|
||||
I included the moc outputs directly into our test class, so that will
|
||||
give it (among other things) a main method so the class can be
|
||||
compiled as an executeable:
|
||||
|
||||
```
|
||||
ADD_EXECUTABLE(qgis_rasterlayertest ${qgis_rasterlayertest_SRCS})
|
||||
ADD_DEPENDENCIES(qgis_rasterlayertest qgis_rasterlayertestmoc)
|
||||
```
|
||||
|
||||
Next we need to specify any library dependencies. At the moment classes
|
||||
have been implemented with a catch-all QT_LIBRARIES dependency, but I will
|
||||
be working to replace that with the specific Qt libraries that each class
|
||||
needs only. Of course you also need to link to the relevant qgis
|
||||
libraries as required by your unit test.
|
||||
|
||||
```
|
||||
TARGET_LINK_LIBRARIES(qgis_rasterlayertest ${QT_LIBRARIES} qgis_core)
|
||||
```
|
||||
|
||||
Next I tell cmake to the same place as the qgis binaries itself. This
|
||||
is something I plan to remove in the future so that the tests can
|
||||
run directly from inside the source tree.
|
||||
|
||||
```
|
||||
INSTALL(TARGETS qgis_rasterlayertest RUNTIME DESTINATION ${QGIS_BIN_DIR})
|
||||
```
|
||||
|
||||
Finally here is where the best magic happens - we register the class with
|
||||
ctest. If you recall in the overview I gave in the beginning of this
|
||||
section we are using both QtTest and CTest together. To recap, **QtTest** adds a
|
||||
main method to your test unit and handles calling your test methods within
|
||||
the class. It also provides some macros like QVERIFY that you can use as
|
||||
to test for failure of the tests using conditions. The output from
|
||||
a QtTest unit test is an executeable which you can run from the command line.
|
||||
However when you have a suite of tests and you want to run each executeable
|
||||
in turn, and better yet integrate running tests into the build process,
|
||||
the **CTest** is what we use. The next line registers the unit test with
|
||||
CMake / CTest.
|
||||
|
||||
```
|
||||
ADD_TEST(qgis_rasterlayertest ${QGIS_BIN_DIR}/qgis_rasterlayertest)
|
||||
```
|
||||
|
||||
The last thing I should add is that if your test requires optional
|
||||
parts of the build process (e.g. Postgresql support, GSL libs, GRASS etc.),
|
||||
you should take care to enclose you test block inside a IF () block
|
||||
in the CMakeLists.txt file.
|
||||
|
||||
|
||||
== Building your unit test ==
|
||||
|
||||
To build the unit test you need only to make sure that ENABLE_TESTS=true
|
||||
in the cmake configuration. There are two ways to do this:
|
||||
|
||||
1. Run ccmake .. (cmakesetup .. under windows) and interactively set
|
||||
the ENABLE_TESTS flag to ON.
|
||||
1. Add a command line flag to cmake e.g. cmake -DENABLE_TESTS=true ..
|
||||
|
||||
Other than that, just build QGIS as per normal and the tests should build
|
||||
too.
|
||||
|
||||
== Run your tests ==
|
||||
|
||||
The simplest way to run the tests is as part of your normal build process:
|
||||
|
||||
```
|
||||
make && make install && make test
|
||||
```
|
||||
|
||||
The make test command will invoke CTest which will run each test that
|
||||
was registered using the ADD_TEST CMake directive described above. Typical
|
||||
output from make test will look like this:
|
||||
|
||||
```
|
||||
Running tests...
|
||||
Start processing tests
|
||||
Test project /Users/tim/dev/cpp/qgis/build
|
||||
1/ 3 Testing qgis_applicationtest ***Exception: Other
|
||||
2/ 3 Testing qgis_filewritertest *** Passed
|
||||
3/ 3 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
|
||||
```
|
||||
|
||||
If a test fails, you can use the ctest command to examine more
|
||||
closely why it failed. User the -R option to specify a regex for
|
||||
which tests you want to run and -V to get verbose output:
|
||||
|
||||
```
|
||||
[build] 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
|
||||
1/ 3 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()
|
||||
Prefix PATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/../
|
||||
Plugin PATH: /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()
|
||||
Prefix PATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/../
|
||||
Plugin PATH: /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
|
||||
|
||||
```
|
||||
|
||||
Well that concludes this section on writing unit tests in QGIS. We hope you
|
||||
will get into the habit of writing test to test new functionality and to
|
||||
check for regressions. Some aspects of the test system (in particular the
|
||||
CMakeLists.txt parts) are still being worked on so that the testing framework
|
||||
works in a truly platform way. I will update this document as things progress.
|
||||
|
||||
= Authors =
|
||||
|
||||
* Tim Sutton (author and editor)
|
||||
|
Loading…
x
Reference in New Issue
Block a user