Tests unitaires et backtrace sous Windows et Linux


précédentsommairesuivant

I. La bibliothèque de tests unitaires

Diagramme de classes des tests unitaires
Diagramme de classes des 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 noeud 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 noeuds et sera responsable du test du sous-arbre.

Enfin, pour découpler efficament 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 noeuds 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 implementé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 noeud.
  • 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 noeuds fils qui stocke les sous-noeuds 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.

Exemple de tests
Sélectionnez

#include "test_register_function.h"
#include "test_functions.h"

/// Petite fonction de test
void petitTest()
{
  	assertEqual(0U, 0U);
	assertDifferent(1U, 0U);
	assertNotEqual(1U, 0U);
	assertNotDifferent(0U, 0U);
	assertLess(2, 5);
	assertGreater(20U, 10U);
}

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.

commandLine.cpp
Sélectionnez

#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) "\033["#prefix";"#color"m" msg "\033[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.

gui.cpp
Sélectionnez

#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();
}

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2006 Matthieu Brucher. 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.