I. La bibliothèque de tests unitaires▲
Afin de simplifier au maximum la gestion des tests, plusieurs facteurs ont été pris en compte :
-
structure arborescente des tests
- un test par feuille de l'arbre des tests,
- possibilité de tester un sous-arbre ;
- découplage de l'interface graphique et des tests eux-mêmes.
En ce qui concerne le premier point, il suffit d'utiliser un pattern composite. La classe de base décrira n'importe quel nœud dans l'arborescence. Ensuite, une spécialisation en feuille permettra de lancer un test, tandis qu'une spécialisation en branche ou encore composite englobera plusieurs sous-nœuds et sera responsable du test du sous-arbre.
Enfin, pour découpler efficacement les tests d'une interface quelconque, on utilisera un pattern Registry qui sera responsable de l'enregistrement des données. Ce pattern, expliqué ici, utilisera la structure arborescente définie précédemment pour réaliser la bibliothèque complète.
I-A. Description des classes de la structure arborescente▲
I-A-1. TestFunctionStruct▲
La classe de base de la structure arborescente est TestFunctionStruct. Elle définit, entre autres, le nom du test ainsi que plusieurs fonctions virtuelles surchargées par les classes filles :
- unsigned int nbTests() : retourne le nombre de tests dans le sous-arbre, utile pour l'interface graphique ;
- void operator()() : exécute le test ;
- void addTest(QStringList, TestFunctionStruct*) : ajoute un test défini par une liste de sous-chaînes ;
- bool hasLeaves() const {return false;} : indique si le test a des feuilles ;
- std::map<QString, TestFunctionStruct*>& getLeaves() : retourne la carte des nœuds fils – par défaut, cette fonction lève une exception – ;
- std::list<TestFunctionStruct*> getRealTests() : retourne la liste de tous les tests dans les sous-arbres.
La fonction permettant de récupérer les tests dans un sous-arbre est traditionnellement implémentée à l'aide du pattern Visiteur. Pour la simplicité de la bibliothèque, ce pattern n'a pas été utilisé, on retourne simplement une liste, celle-ci pourra être utilisée pour autre chose qu'effectuer les tests.
I-A-2. TestSuite▲
Cette classe hérite publiquement de TestFunctionStruct, mais dépend d'un paramètre template. Cet argument permet de spécifier une fonction qui sera appelée et exécutée lors de l'appel à operator()(). Si vous préférez utiliser des foncteurs, c'est cette classe que vous devrez modifier/copier.
- Le constructeur : outre l'initialisation de la classe parente avec le nom du test, la création d'une instance de TestSuite entraîne automatiquement son enregistrement dans le registre, le TestManager.
- unsigned int nbTests() : retourne 1, car un test est défini ici.
- void operator()() : exécute la fonction.
- std::list<TestFunctionStruct*> getRealTests() : retourne une liste à un seul élément.
I-A-3. TestComposite▲
Cette classe est la colle, le lien entre les différents tests.
- Le constructeur : ce constructeur, très simple, initialise la classe mère ainsi que la variable interne nbTests à 0. En effet, au départ, il n'y a aucun test dans ce nœud.
- unsigned int nbTests() : retourne nbTests.
- void operator()() : ne fait rien.
- bool hasLeaves() const {return true;} : retourne true.
- std::map<QString, TestFunctionStruct*>& getLeaves() : retourne la carte des nœuds fils qui stocke les sous-nœuds existants.
- std::list<TestFunctionStruct*> getRealTests() : retourne une liste construite récursivement contenant tous les tests du sous-arbre.
- void addTest(QStringList path, TestFunctionStruct* test) : cette fonction spécifique à TestComposite incrémente le nombre de tests dans l'arbre et ajoute récursivement le test dans le sous-arbre.
Contrairement à l'exemple fourni du pattern Registry, il n'y a pas d'utilité à la recherche directe d'un élément dans le registre, donc cette fonction est inutile ; le test est directement l'élément enregistré dans l'arbre, aucun encapsuleur n'est utilisé, pour simplifier l'arborescence, et l'ajout de tests est fait de manière récursive, pour ajouter en modularité. Enfin, le nom complet du test est enregistré, une fois de plus contrairement à ce qui était proposé dans le pattern Registry, afin d'afficher facilement le nom complet du test.
I-B. Le registre▲
Implémenté sous la forme d'un singleton, cette classe est responsable de la gestion de la structure arborescente. On peut considérer que cette classe est un registre dans lequel on peut enregistrer et récupérer des informations hiérarchiques.
- Le constructeur : protégé, il crée le premier élément dans l'arbre des tests.
- static TestManager& getInstance() : retourne le singleton.
- TestComposite* getRoot() : retourne la racine de l'arbre des tests.
- TestFunctionStruct* getTestFunctionStructFromQStringList(QStringList path) : retourne l'élément dans l'arbre décrit par le chemin path.
- void registerFunctions(const QString& name, TestFunctionStruct* f) : est chargée d'enregistrer au bon endroit dans l'arbre le nouveau test. Agit récursivement à l'aide d'addTests disponible dans TestComposite.
I-C. Proposer un test▲
Maintenant que les tests peuvent être enregistrés automatiquement, il est temps de créer une véritable fonction de test.
I-C-1. Les tests d'assertion▲
Une assertion va tester une condition - par exemple une égalité ou une inégalité -. Elle utilisera les méthodes associées aux instances des classes comparées. Lorsque la comparaison échouera, on lancera une exception particulière indiquant le type d'erreur, la fonction où l'erreur est survenue, la ligne… En ce qui concerne ces derniers éléments, on utilisera une macro du préprocesseur, __FILE__, __LINE__ et __FUNCTION__, pour s'occuper du travail. Pour éviter d'avoir à écrire ces macros, on encapsule l'appel à la fonction de test dans une macro.
I-C-2. La gestion de plusieurs tests▲
Lorsqu'on lance plusieurs tests, on peut obtenir une liste des tests à effectuer grâce à getRealTests(). Ensuite, on parcourt cette liste en lançant chaque test et en récupérant toutes les erreurs, celles attendues grâce à notre exception particulière, et les autres auxquelles on ne s'attendrait pas.
I-D. Lancer les tests▲
On propose de lancer les tests de plusieurs manières. La première, c'est de lancer tous les tests en ligne de commande, et d'afficher les résultats. L'autre solution est de proposer une interface graphique qui permet de sélectionner le sous-arbre à tester et reporter les erreurs et les échecs avec des indications complémentaires si besoin est.
I-E. Code d'exemple▲
Tout d'abord un exemple de test simple.
#include
"test_register_function.h"
#include
"test_functions.h"
///
Petite fonction de test
void
petitTest()
{
assertEqual(0
U, 0
U);
assertDifferent(1
U, 0
U);
assertNotEqual(1
U, 0
U);
assertNotDifferent(0
U, 0
U);
assertLess(2
, 5
);
assertGreater(20
U, 10
U);
}
TestSuite<
petitTest>
TestSuitePerso("Petit:Test"
); // On peut encapsuler cette déclaration dans une macro
Avec cela, il faudra un gestionnaire, donc soit un en ligne de commande, soit avec interface graphique.
I-E-1. Version en ligne de commande▲
Pour l'utiliser, il suffit de compiler avec QCore(d)4.dll.
#include
<exception>
#include
<fstream>
#include
<cstdio>
#include
<iostream>
#include
<string>
#include
"test_register_function.h"
#include
"test_functions.h"
#ifdef Q_OS_WIN
#include
"windows.h"
static
CRITICAL_SECTION outputCriticalSection;
static
HANDLE hConsole =
INVALID_HANDLE_VALUE;
static
WORD consoleAttributes =
0
;
static
const
char
*
qWinColoredMsg(int
prefix, int
color, const
char
*
msg)
{
if
(!
hConsole)
return
msg;
WORD attr =
consoleAttributes &
~
(FOREGROUND_GREEN |
FOREGROUND_BLUE
|
FOREGROUND_RED |
FOREGROUND_INTENSITY);
if
(prefix)
attr |=
FOREGROUND_INTENSITY;
if
(color ==
32
)
attr |=
FOREGROUND_GREEN;
if
(color ==
31
)
attr |=
FOREGROUND_RED |
FOREGROUND_INTENSITY;
if
(color ==
37
)
attr |=
FOREGROUND_RED |
FOREGROUND_GREEN |
FOREGROUND_BLUE;
if
(color ==
33
)
attr |=
FOREGROUND_RED |
FOREGROUND_GREEN;
SetConsoleTextAttribute(hConsole, attr);
printf(msg);
SetConsoleTextAttribute(hConsole, consoleAttributes);
return
""
;
}
# define COLORED_MSG(prefix, color, msg) qWinColoredMsg(prefix, color, msg)
#else
# define COLORED_MSG(prefix, color, msg)
"
\0
33["
#prefix
";"
#color
"m"
msg
"
\0
33[0m"
#endif
void
iterateThroughTests(TestFunctionStruct*
test)
{
if
(test->
hasLeaves())
{
std::
map<
QString, TestFunctionStruct*>&
leaves =
test->
getLeaves();
for
(std::
map<
QString, TestFunctionStruct*>
::
iterator it =
leaves.begin(); it !=
leaves.end(); ++
it)
{
iterateThroughTests(it->
second);
}
}
else
{
try
{
(*
test)();
std::
cout <<
COLORED_MSG(0
, 32
, "PASS "
) <<
test->
functionName.toStdString() <<
std::
endl;
}
catch
(const
Tester::
Thrown&
failure)
{
std::
cout <<
COLORED_MSG(0
, 31
, "FAIL! "
) <<
test->
functionName.toStdString() <<
std::
endl;
std::
cout <<
"* "
<<
failure.explanation.toStdString() <<
std::
endl;
}
catch
(const
std::
exception&
exception)
{
std::
cout <<
COLORED_MSG(0
, 31
, "FAIL! "
) <<
test->
functionName.toStdString() <<
std::
endl <<
" "
<<
exception.what() <<
std::
endl;
}
catch
(...)
{
std::
cout <<
COLORED_MSG(0
, 37
, "ERROR! "
) <<
test->
functionName.toStdString() <<
std::
endl <<
" Unknown error"
<<
std::
endl;
}
}
}
int
main(int
argc, char
**
argv)
{
TestManager&
manager =
TestManager::
getInstance();
iterateThroughTests(manager.getRoot());
return
EXIT_SUCCESS;
}
I-E-2. Version avec interface graphique▲
Ici, il faudra compiler avec test_gui.h et test_gui.cpp, le tout en liant avec QGui(d)4.dll ainsi que QCore(d)4.dll.
#define PACKAGE_STRING
"Exemple"
#include
<exception>
#include
<fstream>
#include
<QtGui/QtGui>
#include
"testGui.h"
int
main(int
argc, char
**
argv)
{
QApplication app(argc, argv);
Tester::
TestMainWindow mainWin(QString::
fromAscii(PACKAGE_STRING));
mainWin.show();
return
app.exec();
}