Introduction au module QtTest

| Rémi Achard | dourouc05 |

Le module QtTest permet de réaliser simplement ses tests unitaires et benchmark avec le framework Qt. Ce tutoriel s'inspire fortement de la série de mini tutoriels présents dans la documentation Qt.

N'hésitez pas à commenter cet article !
Commentez

- Titre : Introduction au module QtTest

- Auteurs : Rémi Achard, dourouc05

- Parution : 16 septembre 2010

- Mise à jour : 29 novembre 2010

- Licence : Copyright ® 2010 Rémi Achard. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Test unitaire : le code minimal

- II. Exécuter un test sur un ensemble de données

- III. Benchmark

- IV. Conclusion

I - Test unitaire : le code minimal

Nous allons commencer par écrire un test simple permettant de vérifier que la fonction QString::toUpper copie correctement la chaine de caractères en convertissant en majuscule.

testqstring.cpp

#include <QtTest/QtTest>

class TestQString: public QObject
{
Q_OBJECT

private slots:
void toUpper();
};

void TestQString::toUpper()
{
QString str = "Hello";
QCOMPARE(str.toUpper(), QString("HELLO"));
}

QTEST_MAIN(TestQString)
#include "testqstring.moc"

La classe de test doit hériter de QObject. Les tests doivent impérativement être déclarés en tant que slots privés pour que QtTest puisse automatiquement les reconnaître et les exécuter.

La macro QCOMPARE vérifie que les deux objets passés en argument sont bien identiques et, dans le cas contraire, affiche la valeur de ces deux objets, ce qui facilite le débogage.

testqstring.cpp

void TestQString::toUpper()
{
QString str = "Hello";
QCOMPARE(str.toUpper(), QString("HELLO"));
}

A noter que cette macro est assez stricte : les objets comparés doivent être de même type. Par exemple, la comparaison entre un objet QString et une chaîne de caractères constante provoquera une erreur à la compilation.

testqstring.cpp

// Erreur à la compilation
void TestQString::toUpper()
{
QString str = "Hello";
QCOMPARE(str.toUpper(), "HELLO");
}

La macro QTEST_MAIN génère une fonction main exécutant tous les tests qui ont été déclarés. De plus, dans le cas présent, comme nous avons déclaré et implémenté les tests dans un fichier source (.cpp), il faut inclure le fichier généré par Qt en interne (.moc).

testqstring.cpp

QTEST_MAIN(TestQString)
#include "testqstring.moc"

Pour la compilation avec qmake, il suffit de rajouter dans le .pro de votre projet :

testqstring.pro

QT += testlib

Il est possible de modifier le comportement d'exécution par défaut des tests en passant des arguments à l'exécutable généré. Par exemple (voir la documentation pour une liste exhaustive) :

- -silent : affichage minimal, signale seulement les erreurs ;

- -xml : génère les résultats en xml ;

- -functions : liste toutes les fonctions présentes dans le test.

Voici la sortie générée par l'exemple :

********* Start testing of TestQString *********
Config: Using QTest library 4.7.0, Qt 4.7.0
PASS : TestQString::initTestCase()
PASS : TestQString::toUpper()
PASS : TestQString::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of TestQString *********

La quatrième ligne correspond au test de toUpper et indique que celui-ci s'est correctement déroulé :

PASS : TestQString::toUpper()

Comme on peut le remarquer, deux tests supplémentaires ont été éxecutés :

PASS : TestQString::initTestCase()
(...)
PASS : TestQString::cleanupTestCase()

En effet, QtTest génère automatiquement ces deux tests qui permettent aux développeurs d'initialiser et de libérer les ressources utilisées dans les différents tests unitaires :

- initTestCase() : est appelé avant l'exécution du premier test ;

- cleanupTestCase() : est appelé après l'exécution du dernier test.

II - Exécuter un test sur un ensemble de données

QtTest permet de définir un ensemble de données (scénarios) pour un test, puis de réaliser le test sur chaque scénario défini.

Voici le résultat appliqué à l'exemple précédent :

testqstring.cpp

#include <QtTest/QtTest>

class TestQString: public QObject
{
Q_OBJECT

private slots:
void toUpper_data();
void toUpper();
};

void TestQString::toUpper_data()
{
QTest::addColumn<QString>("string");
QTest::addColumn<QString>("result");

QTest::newRow("tout en minuscules") << "hello" << "HELLO";
QTest::newRow("minuscules et majuscules") << "Hello" << "HELLO";
QTest::newRow("tout en majuscule") << "HELLO" << "HELLO";
}

void TestQString::toUpper()
{
QFETCH(QString, string);
QFETCH(QString, result);

QCOMPARE(string.toUpper(), result);
}

QTEST_MAIN(TestQString)
#include "testqstring.moc"

Dans un premier temps, il faut déclarer une fonction reprenant le nom du test suivi du suffixe _data :

testqstring.cpp

void TestQString::toUpper_data()

QtTest fournit un mécanisme simple pour stocker les données des scénarios sous forme de tableau : on déclare d'abord la structure du tableau, ici deux colonnes, une contenant la chaîne à tester et l'autre le résultat attendu.

testqstring.cpp

QTest::addColumn<QString>("string");
QTest::addColumn<QString>("result");

Les noms de ces deux colonnes serviront à la macro QFETCH pour récupérer les données associées.

Il ne reste plus qu'à remplir le tableau, via l'opérateur <<. La fonction newRow prend le nom du scénario en argument, qui sera affiché en cas d'échec du test.

testqstring.cpp

QTest::newRow("tout en minuscules") << "hello" << "HELLO";
QTest::newRow("minuscules et majuscules") << "Hello" << "HELLO";
QTest::newRow("tout en majuscule") << "HELLO" << "HELLO";

Pour terminer, la fonction de test a besoin d'être modifiée pour récupérer les données automatiquement à l'aide de la macro QFETCH (type attendu, nom de la colonne).

testqstring.cpp

QFETCH(QString, string);
QFETCH(QString, result);

La fonction TestQString::toUpper() sera donc appelée trois fois, puisque le tableau contient trois lignes (scénarios).

III - Benchmark

Pour créer un benchmark, il suffit d'utiliser la macro QBENCHMARK dans un test.

testbenchmark.cpp

#include <QtTest/QtTest>

class TestBenchmark: public QObject
{
Q_OBJECT

private slots:
void simple();
};

void TestBenchmark::simple()
{
QString str = " Hello   ";

QBENCHMARK {
str.trimmed();
}
}

QTEST_MAIN(TestQString)
#include "testbenchmark.moc"

Le code contenu dans la macro sera exécuté plusieurs fois si nécessaire, afin d'obtenir un temps moyen d'exécution le plus précis possible. Ici, nous aurons donc le temps moyen mis par la fonction trimmed() pour s'exécuter (la fonction trimmed() permet de retirer les espaces présents en début et fin de chaîne).

Voici la sortie générée par l'exemple :

********* Start testing of TestBenchmark *********
Config: Using QTest library 4.7.0, Qt 4.7.0
PASS : TestBenchmark::initTestCase()
RESULT : TestBenchmark::simple():
0.000190 msecs per iteration (total: 100, iterations: 524288)
PASS : TestBenchmark::simple()
PASS : TestBenchmark::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of TestBenchmark *********

Il possible de choisir la méthode de comptage utilisée par QtTest pour estimer le temps moyen d'exécution, ceci par le biais d'arguments de la ligne de commande.

Nom

Argument de la ligne de commande

Plateforme disponible

Temps d'exécution

(défaut)

Toutes les plateformes

Compteur de ticks CPU

-tickcounter

Windows, Mac OS X, Linux, beaucoup de systèmes UNIX-like

Valgrind/Callgrind

-callgrind

Linux (si installé)

Compteur d'événements

-eventcounter

Toutes les plateformes

IV - Conclusion

Merci à littledaem pour sa relecture et ses conseils.

Pour aller plus loin, le module QtTest permet également de tester les applications graphiques, en simulant par exemple un clic de souris, ou de tester les signaux émis par les objets de Qt.