Chez SparkPost, nous consacrons beaucoup de temps et d'efforts à tester notre code. Notre plate-forme est écrite en C, et récemment j'ai recherché l'intégration avec un cadre de tests unitaires appelé "CppUTest", qui fournit des tests de style xUnit pour C/C++. Ce cadre est robuste, riche en fonctionnalités et en développement actif, ce qui en fait un excellent choix. Il fournit également une couche d'intégration C, ce qui le rend facile à utiliser avec notre code C de plate-forme même si la plupart du cadre est en C++. Ce tutoriel couvre comment commencer avec CppUTest sur vos propres projets.
Téléchargement de CppUTest
La page de projet CppUTest est disponible ici, et le référentiel se trouve sur github. Il est également inclus dans les dépôts de gestion de paquet pour de nombreuses distributions linux, ainsi que dans homebrew sur Mac OS. Les exemples qui suivent ont été exécutés sur Mac OS X, mais ils proviennent de code écrit pour Red Hat, le système d'exploitation sur lequel notre plate-forme fonctionne.
Les bases sont bien documentées sur la page d'accueil de CppUTest. Nous allons passer rapidement par cela et aborder certaines des fonctionnalités les plus intéressantes.
Poser les Fondations
La première chose à faire est d'écrire un peu de code !
Notre projet de test aura un fichier 'main' et inclura une bibliothèque utilitaire appelée 'code'. La bibliothèque fournira une fonction simple qui retourne 1 (pour l'instant). Les fichiers seront disposés comme ceci :
├── src │ ├── code │ │ ├── code.cpp │ │ └── code.h │ └── main.cpp └── t ├── main.cpp └── test.cpp
Commençons par écrire les fichiers src/
// src/main.cpp #include <stdlib.h> #include <stdio.h> #include "code.h" int main(void) { test_func(); printf("hello world!\n"); exit(0); }
// src/code/code.cpp #include <stdlib.h> #include "code.h" int test_func () { return 1; }
// src/code/code.h #ifndef __code_h__ #define __code_h__ int test_func (); #endif
Maintenant, faisons les tests, qui se trouveront dans le répertoire t/. La première chose à faire est de configurer un exécuteur de test qui exécutera nos fichiers de test. C'est également la fonction 'main' qui s'exécutera une fois que tout cela sera compilé :
// t/main.cpp #include "CppUTest/CommandLineTestRunner.h" int main(int ac, char** av) { return CommandLineTestRunner::RunAllTests(ac, av); }
Maintenant nous pouvons écrire notre premier module de test :
// t/test.cpp #include "CppUTest/TestHarness.h" #include "code.h" TEST_GROUP(AwesomeExamples) { }; TEST(AwesomeExamples, FirstExample) { int x = test_func(); CHECK_EQUAL(1, x); }
Ensuite, nous devons écrire des makefiles. Nous aurons besoin de deux : un pour les fichiers du projet sous src/, et un pour les tests.
Makefile du Projet
Le makefile du projet sera au même niveau que les répertoires 'src' et 't' à la racine du projet. Il devrait ressembler à ceci :
# Makefile SRC_DIR=./src CODE_DIR=$(SRC_DIR)/code OUT=example TEST_DIR=t test: make -C $(TEST_DIR) test_clean: make -C $(TEST_DIR) clean code.o: gcc -c -I$(CODE_DIR) $(CODE_DIR)/code.cpp -o $(CODE_DIR)/code.o main: code.o gcc -I$(CODE_DIR) $(CODE_DIR)/code.o $(SRC_DIR)/main.cpp -o $(OUT) all: test main clean: test_clean rm $(SRC_DIR)/*.o $(CODE_DIR)/*.o $(OUT)
Notez que cela utilise 'make -C' pour les cibles de test – ce qui signifie qu'il appellera 'make' à nouveau en utilisant le makefile dans le répertoire de test.
À ce stade, nous pouvons compiler le code 'src' avec le makefile et voir que cela fonctionne :
[]$ make main gcc -c -I./src/code ./src/code/code.cpp -o ./src/code/code.o gcc -I./src/code ./src/code/code.o ./src/main.cpp -o example []$ ./example hello world!
Makefile des Tests
Pour les tests, les choses sont un peu plus complexes car nous devons charger et intégrer correctement la bibliothèque CppUTest.
Le référentiel CppUTest fournit un fichier appelé "MakefileWorker.mk". Il offre de nombreuses fonctionnalités qui simplifient la construction avec CppUTest. Le fichier se trouve dans le répertoire "build" du référentiel git. Pour ce tutoriel, nous allons supposer qu'il a été copié dans le répertoire 't/'. Il peut être utilisé comme suit :
# nous ne voulons pas utiliser de chemins relatifs, donc nous définissons ces variables PROJECT_DIR=/path/to/project SRC_DIR=$(PROJECT_DIR)/src TEST_DIR=$(PROJECT_DIR)/t # spécifiez où le code source et les inclusions sont situés INCLUDE_DIRS=$(SRC_DIR)/code SRC_DIRS=$(SRC_DIR)/code # spécifiez où le code des tests est situé TEST_SRC_DIRS = $(TEST_DIR) # quel nom donner au binaire de test TEST_TARGET=example # où se trouve la bibliothèque cpputest CPPUTEST_HOME=/usr/local # exécuter MakefileWorker.mk avec les variables définies ici include MakefileWorker.mk
Notez que CPPUTEST_HOME doit être défini sur l'endroit où CppUTest a été installé. Si vous avez installé un package de distribution, cela sera généralement sous /usr/local sur un système linux/mac. Si vous avez vérifié le dépôt vous-même, c'est l'endroit où ce dépôt se trouve.
Ces options sont toutes documentées dans MakefileWorker.mk.
MakefileWorker.mk ajoute également quelques cibles de makefile, y compris les suivantes :
all – construit les tests indiqués par le makefile
clean – supprime tous les fichiers objets et gcov générés pour les tests
realclean – supprime tous les fichiers objets ou gcov dans l'ensemble de l'arbre de répertoires
flags – liste tous les drapeaux configurés utilisés pour compiler les tests
debug – liste tous les fichiers source, objets, dépendances et ‘choses à nettoyer’
Couverture de Code
Les tests unitaires ne seraient pas complets sans un rapport de couverture. L'outil de référence pour cela pour les projets utilisant gcc est gcov, disponible dans l'ensemble de la suite d'utilitaires gcc. Cpputest s'intègre facilement à gcov, il vous suffit d'ajouter cette ligne au makefile :
CPPUTEST_USE_GCOV=Y
Ensuite, nous devons nous assurer que le script filterGcov.sh du ce dépôt se trouve dans ‘/scripts/filterGcov.sh’ par rapport à l'endroit où vous avez défini ‘CPPUTEST_HOME’. Il doit également avoir des permissions d'exécution.
Dans l'exemple de Makefile, il serait déployé à ‘/usr/local/scripts/filterGcov.sh’. Si vous exécutez CppUTest depuis un checkout de dépôt, tout devrait fonctionner sans modification.
Avec cela en place, vous pouvez simplement exécuter ‘make gcov’ et l'analyse sera générée pour vous. Dans notre cas, nous devrons 'make -B' pour reconstruire les fichiers objets avec gcov activé :
[]$ make -B gcov < sortie de compilation > pour d dans /Users/ykuperman/code/blogpost/qa/src/code ; do \ FILES=`ls $d/*.c $d/*.cc $d/*.cpp 2> /dev/null` ; \ gcov --object-directory objs/$d $FILES >> gcov_output.txt 2>>gcov_error.txt ; \ done pour f dans ; do \ gcov --object-directory objs/$f $f >> gcov_output.txt 2>>gcov_error.txt ; \ done /usr/local/scripts/filterGcov.sh gcov_output.txt gcov_error.txt gcov_report.txt example.txt cat gcov_report.txt 100.00% /Users/ykuperman/code/blogpost/qa/src/code/code.cpp mkdir -p gcov mv *.gcov gcov mv gcov_* gcov Voir le répertoire gcov pour plus de détails
Cela produira un certain nombre de fichiers dans un nouveau répertoire ‘gcov’. Ceux-ci sont :
code.cpp.gcov – le véritable fichier ‘gcov’ pour le code testé
gcov_error.txt – un rapport d'erreur (dans notre cas, il devrait être vide)
gcov_output.txt – la véritable sortie de la commande gcov qui a été exécutée
gcov_report.txt – un résumé de la couverture pour chaque fichier sous test
gcov_report.txt.html – une version html de gcov_report
Détection des Fuites de Mémoire Cpputest
Cpputest vous permet de détecter automatiquement les fuites de mémoire en redéfinissant la famille de fonctions standard “malloc/free” pour utiliser ses propres wrappers à la place. Cela lui permet de détecter rapidement les fuites et de les signaler pour chaque exécution de test. Cela est activé par défaut dans MakefileWorker.mk, donc c'est déjà en place avec les étapes décrites jusqu'à présent.
Pour illustrer, créons une fuite de mémoire dans test_func() !
Pour revenir à code.c, nous ajoutons un malloc() à la fonction, comme ceci :
int test_func() { malloc(1); return 1; }
Maintenant, après recompilation, l'erreur suivante est produite :
test.cpp:9: erreur : Échec dans TEST(AwesomeExamples, FirstExample) Fuite(s) de mémoire trouvée(s). Nombre d'allocations (4) Taille de la fuite : 1 Alloué à : ./code.c et ligne : 6. Type : "malloc" Mémoire : <0x7fc9e94056d0> Contenu : 0000: 00 |.| Nombre total de fuites : 1
Cela montre quel test a causé la fuite, où la fuite s'est produite dans le code source, et ce qu'il y avait dans la mémoire fuitée. Très utile !
Il existe quelques mises en garde avec cette fonctionnalité :
Cpputest utilise des macros préprocesseur pour redéfinir dynamiquement tous les appels aux fonctions standard de gestion de la mémoire. Cela signifie qu'il ne fonctionnera que pour les appels dans le code source testé, puisque c'est ce qui est compilé avec les substitutions de CppUTest. Les fuites dans les bibliothèques liées ne seront pas détectées.
Parfois, la mémoire qui est allouée pour toute la durée du processus n'est pas destinée à être libérée. Cela peut créer beaucoup d'erreurs inesthétiques si vous testez un module avec ce comportement. Pour désactiver la détection des fuites, vous pouvez faire ceci :
CPPUTEST_USE_MEM_LEAK_DETECTION=N
Intéressé par Plus ?
Ceci n'est que la pointe de l'iceberg en ce qui concerne toutes les fonctionnalités contenues dans cet outil. En plus des bases discutées ici, il dispose également d'un cadre de simulation, d'une couche d'intégration C directe et d'un cadre de plugins, pour n'en nommer que quelques-unes significatives. Le dépôt contient également un répertoire entier de scripts d'aide qui peuvent aider à automatiser certaines des parties routinières du travail avec le cadre.
J'espère que les informations ici vous aideront à améliorer la qualité de votre code C/C++ avec cet excellent outil !