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
Sélectionnez
#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
Sélectionnez
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
Sélectionnez
// 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
Sélectionnez
QTEST_MAIN(TestQString)
#include "testqstring.moc"

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

testqstring.pro
Sélectionnez
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 :

 
Sélectionnez
********* 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é :

 
Sélectionnez
PASS : TestQString::toUpper()

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

 
Sélectionnez
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
Sélectionnez
#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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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 de d'utiliser la macro QBENCHMARK dans un test.

testbenchmark.cpp
Sélectionnez
#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 :

 
Sélectionnez
********* 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.