Après avoir soliloqué sur l’architecture de tests Qt / C++ que j’aimerai obtenir, commençons par le commencement : créer un test avec QTestLib et l’exécuter. A vrai dire, cet article traine dans mes brouillons depuis au moins 2014, mais je me suis décidé à le publier. Les liens datent un peu, mais tout reste très fonctionnel.
Un mini rappel sur les tests unitaires :
- Un test case par classe : ce test case testera les différentes fonctions de cette classe
- dans des cas nominaux,
- des cas d’erreurs,
- voire des cas limites.
- Tester le plus petit bloc possible : si la classe à tester utilise d’autres composants, on dit qu’elle a des « dépendances », il faut alors « mocker » ces dépendances (si possible) ou en tout cas les maîtriser au maximum. Ce sont les integration tests qui testeront plusieurs blocs ensemble.
Créer un test case
Avec QTestLib, un test case est un QObject classique. La classe de test contient plusieurs slots privés (i.e. « private slots », un slot public ne sera pas exécuté lors du passage des tests) qui seront exécutés les uns à la suite des autres. La documentation de Qt fournit aussi un exemple de test case, mais nous en verrons d’autres plus loin.
Dans chaque test, on va utiliser des assertions, permettant de déterminer si un test est en Success ou Failure. On place dans l’assertion ce que l’on considère comme le bon comportement pour un bout de code selon le contexte donné. En enchaînant plusieurs assertions, on est censé tester un morceau code dans tous les contextes possibles.
QVERIFY(bool assessmentExpectedTrue)
: « assessmentExpectedTrue » doit être vrai pour que le test soit en SuccessQVERIFY2(bool assessmentExpectedTrue, char *descriptionIfItFailed)
: Idem mais on peut afficher un message de debug si l’assertion est fauxQCOMPARE(actual, expected)
: Idem, sauf que cette fois-ci la valeur obtenue (actual) et attendue (expected) doivent être égales. L’avantage, c’est que ces valeurs apparaissent dans les logs si le test échoue (ce qui n’est pas le cas avec QVERIFY) et c’est bien pratique pour le debug ! A noter : il faut que l’opérateur « == » et l’opérateurQDebug operator<<(QDebug dbg, T val)
soit définie pour les types complexes.
En plus des slots privés qui représentent chacun un test, on peut définir au besoin 4 méthodes particulières dans le test case :
- initTestCase (static) : exécuté au tout début du test case.
- cleanupTestCase (static) : exécuté à la toute fin du test case.
- init : exécuté avant chaque méthode de test. On parle parfois de « set up » ou « préambule ».
- cleanup : exécuté après chaque méthode de test. On parle parfois de « tear down » ou « postambule »
Au niveau des bonnes pratiques, pour ma part je reprends les notations d’autres moteurs de tests (PHPUnit et JUnit), et donc j’aime bien :
- préfixer le nom des test cases par « Test ». Pour une classe
MyClass
→MyClassTest
. - préfixer le nom des tests (les private slots) par « test ». Ex : « testAdd » et « testAddWithErrors » pour tester la méthode
MyClass::add
.
Voici un exemple de test case, le header et sa source :
[cpp title="MyClassTest.h"]
#ifndef MYCLASSTEST_H
#define MYCLASSTEST_H
#include <QObject>
class MyClassTest : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void cleanupTestCase();
void init();
void cleanup();
void testHelloThere();
};
#endif // MYCLASSTEST_H
[/cpp]
[cpp title="MyClassTest.cpp"]
#include "MyClassTest.h"
#include <QDebug>
#include <QTest>
#include "MyClass.h"
void MyClassTest::initTestCase()
{
qDebug()<<"Test is starting";
}
void MyClassTest::cleanupTestCase()
{
qDebug()<<"Test is finished";
}
void MyClassTest::init()
{
qDebug()<<"init";
}
void MyClassTest::cleanup()
{
qDebug()<<"cleanup";
}
void MyClassTest::testHelloThere()
{
MyClass myClass;
QVERIFY("Hello!" == myClass.helloThere());
QVERIFY2("Hello Martin!" == myClass.helloThere("Martin"), "Martin should have been saluted...");
QCOMPARE(myClass.helloThere("Riggs"), QStringLiteral("Hello Riggs!"));
}
[/cpp]
Voilà qui est bien beau, mais voyons voir comment exécuter ce bout de test.
Exécuter un test case
Lancer un test case se fait relativement simple via la méthode qExec. Un petit exemple qui se contente d’exécuter le test case « MyClassTest » et d’afficher le résultat :
[cpp]
#include <QDebug>
#include <QTest>
#include "MyClassTest.h"
int main(int argc, char **argv)
{
MyClassTest testCase;
int testCaseResult = QTest::qExec(&testCase, argc, argv);
qDebug()<<"MyClassTest: "<<(0 == testCaseResult ? "Success" : "Failure");
return testCaseResult;
}
[/cpp]
Voici qui devrait afficher :
********* Start testing of MyClassTest *********
Config: Using QtTest library 5.3.2, Qt 5.3.2
QDEBUG : MyClassTest::initTestCase() Test is starting
PASS : MyClassTest::initTestCase()
QDEBUG : MyClassTest::testHelloThere() init
QDEBUG : MyClassTest::testHelloThere() cleanup
PASS : MyClassTest::testHelloThere()
QDEBUG : MyClassTest::cleanupTestCase() Test is finished
PASS : MyClassTest::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of MyClassTest *********
MyClassTest: Success
Lancer une série de test cases
Allons plus loin en se fabriquant un petit outil, un test suite runner, qui va lancer plusieurs test cases d’un coup. Pour se faire nous allons créer une classe TestRunner
. Chaque test case aura un log séparé, et le TesterRunner doit décrire globalement le statut de chaque test case (passed / failure), ainsi que le statut du test suite (passed / failure) c’est-à-dire un « ET » logique sur le résultat de tous les test cases.
[code file="TestRunner.h" language="cpp"]
#ifndef TESTRUNNER_H
#define TESTRUNNER_H
#include <QHash>
#include <QTimer>
#include <QDateTime>
#include <QCoreApplication>
#include <QtTest>
#include "Logger.h"
class TestRunner: public QObject
{
Q_OBJECT
public:
Logger logTestsResult;
TestRunner() : overallResult(0)
{
logTestsResult = Logger("TestRunner", "testsresult_", Logger::DontAddToGeneralLog);
logTestsResult.setFolder("result");
logTestsResult.cleanAll();
}
void addTest(QString testCaseName, QObject * testCase) {
testCase-&gt;setParent(this);
testCaseList.insert(testCaseName, testCase);
}
bool runTests() {
int argc =0;
char * argv[] = {0};
QCoreApplication app(argc, argv);
QTimer::singleShot(0, this, SLOT(run()));
app.exec();
return overallResult == 0;
}
private slots:
void run() {
// Launch
doRunTests();
// Log overall result
logTestsResult.i(0 == overallResult ? "Passed" : "Failure");
// Quit
QApplication::instance()-&gt;quit();
}
private:
void doRunTests() {
foreach (QString testCaseName, testCaseList.keys()) {
QStringList outputLogfileCmd;
outputLogfileCmd<<" "<<"-o"<<logTestsResult.getFolder()+testCaseName+".log";
int testCaseResult = QTest::qExec(testCaseList.value(testCaseName), outputLogfileCmd);
logTestsResult.i(testCaseName+": "+(0 == testCaseResult ? "Passed" : "Faillure"));
overallResult |= testCaseResult;
}
}
QHash<QString, QObject *> testCaseList;
int overallResult;
};
#endif // TESTRUNNER_H
[/code]
[code file="TestRunner.cpp" language="cpp"]
#include "TestRunner.h"
#include "tst_ExampleTest.h"
#include <QString>
int main() {
TestRunner testRunner;
testRunner.addTest("ExampleTest", new ExampleTest());
testRunner.runTests();
return 0;
}
[/code]
[code file="tst_ExampleTest.h" language="cpp"]
#ifndef TST_EXAMPLETEST_H
#define TST_EXAMPLETEST_H
#include <QtCore/QString>
#include <QtTest/QtTest>
class ExampleTest: public QObject
{
Q_OBJECT
public:
ExampleTest(bool debugMode=false);
private Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void testEx1();
};
#endif // TST_EXAMPLETEST_H
[/code]
[code file="tst_ExampleTest.cpp" language="cpp"]
#include "tst_ExampleTest.h"
ExampleTest::ExampleTest()
{
}
void ExampleTest::initTestCase()
{
}
void ExampleTest::cleanupTestCase()
{
}
void ExampleTest::testEx1()
{
QVERIFY2(true, "Should be true");
}
[/code]
L’environnement de tests de mes rêves
Voici ma liste personnelle du test suite runner de mes rêves en C++/Qt :
- Pas de TestRunner à créer, juste des classes de tests (héritant d’un type spécifique, ou au pire s’inscrivant quelque part)
- Lancement des tests unitaires au choix lors de la compilation (grâce à une config ou un define)
- Résultat des tests (par test case et résultat global) dans des fichiers de logs, et/ou au choix dans stdout (coloré dans ce cas). En utilisant exclusivement qDebug bien sûr, ou stdout, pour ne pas inclure de bibliothèque supplémentaire.
- Fonction de setup/teardown par test case, et avant chaque test
- Supporter les fixtures (les quoi ? cf l’excellent article Increase your QTest productivity)
- Tester les signaux / slots Qt avec QSignalSpy (outil indispensable pour les tests unitaires Qt !)
- Ajouter une librairie pour gérer facilement les stub / mock
La bonne nouvelle c’est que j’ai partiellement atteint ce rêve (ohoh) avec un jeu de classes TestRunner
et IUnitTest
, empaqueté dans un package Qompoter appelé autotest
pour l’utiliser facilement. C’est ce que l’on va voir au prochain épisode 🙂
Plus d’informations
- Increase your QTest productivity – soulève le problème du TestRunner QtTestLib classique, et propose une solution !
- Running multiple unit test – Idem, le TestRunner utilisé généralement pour QTestLib doit être modifié à chaque ajout d’un test case. Encore une autre solution est proposée.
- Stack Over flow : What unit testing framework should I use for Qt? – aborde le sujet du test runner avec QTestLib
- How to redirect Valgrinds output to a file? – Rediriger output dans un fichier
- QTestLib – Unit testing with Qt
- QTestlib Tutorial
- QTestlib User Manual
- L’intéressante doc de QTest et QTestLib Manual version Qt 4.7, mais étrangement, cela a l’air plus complet par rapport à la précédente… est-ce une illusion ?
- Some principles to write Unit tests
- About Unit Testing. Tout un tas d’exemple avec QtTestLib.
- Comment faire apparaître les résultats de vos tests (UnitTest++) directement dans l’interface de QtCreator.