Prise en main avec CppUTest

Oiseau

14 mai 2017

Email

1 min read

Prise en main avec CppUTest

Points Clés

    • CppUTest est un framework de test de style xUnit léger et activement maintenu pour C/C++, avec une couche d'intégration C qui fonctionne parfaitement même dans les bases de code lourdes en C.

    • Vous pouvez l'installer via les gestionnaires de paquets (distributions Linux, Homebrew) ou cloner le dépôt GitHub.

    • Une configuration minimale se compose de :

      • un répertoire de production src/,

      • un répertoire de tests t/,

      • un exécuteur de tests (CommandLineTestRunner), et

      • des modules de test utilisant les blocs TEST_GROUP et TEST().

    • CppUTest fournit un assistant MakefileWorker.mk qui simplifie la compilation des tests, l'édition de liens avec les bibliothèques, et la gestion des indicateurs.

    • La détection des fuites de mémoire est activée par défaut grâce aux remplacements de malloc/free, capturant les fuites au sein du code source testé.

    • La couverture de code via gcov s'intègre facilement en activant CPPUTEST_USE_GCOV=Y, en produisant des rapports de couverture complets et des résumés HTML.

    • Le framework inclut des fonctionnalités avancées : moquerie, plugins, scripts d'aide, et interopérabilité directe avec C — utile pour les bases de code d'entreprise complexes.

Points forts des Q&A

  • Qu'est-ce que CppUTest et pourquoi l'utiliser ?

    C'est un cadre de test robuste de style xUnit pour C/C++ avec une API propre, des macros d'assertion intégrées, une détection des fuites et un développement actif — idéal pour les systèmes hérités ou modernes.

  • Comment structurer un projet de base en utilisant CppUTest?

    src/
      code/
        code.cpp
        code.h
      main.cpp
    t/
      main.cpp (test runner)
      test.cpp (test suite)
  • Comment exécutez-vous tous les tests ?

    Le test runner utilise :

    return CommandLineTestRunner::RunAllTests(ac, av);
  • Comment construire des tests sans configurer manuellement les options du compilateur ?

    Utilisez MakefileWorker.mk de CppUTest, qui gère automatiquement les indicateurs, le lien et l'exécution des tests.

  • CppUTest peut-il détecter les fuites de mémoire automatiquement ?

    Oui. Cela remplace malloc/free lors des constructions de test, rapportant :

    • quel test a fui,

    • où cela s'est produit,

    • la taille de la fuite et le contenu de la mémoire.

    Exemple de sortie d'échec :

    Memory leak(s) found.
    Allocated at: code.c line 6
    Leak size: 1
  • Comment générer un code coverage ?

    1. Activer : CPPUTEST_USE_GCOV=Y

    2. Assurez-vous que filterGcov.sh est disponible à $(CPPUTEST_HOME)/scripts/.

    3. Exécuter : make gcov

      Cela produit des rapports de couverture .gcov, un résumé en texte, et des rapports de couverture HTML.

  • Que peut faire CppUTest d'autre au-delà des tests de base ?

    • système de simulation

    • système de plugins

    • scripts d'automatisation d'assistance

    • intégration native C

    • macros d'assertion étendues

  • Who is CppUTest best suited for?

    Équipes travaillant avec des systèmes embarqués, des plateformes C, des services C++, ou tout environnement où la fiabilité et la sécurité mémoire doivent être continuellement validées.

Chez SparkPost, nous consacrons beaucoup de temps et d'efforts à tester notre code. Notre plateforme est écrite en C, et récemment, j'ai recherché l'intégration avec un framework de test unitaire appelé "CppUTest", qui fournit des tests de style xUnit pour C/C++. Ce framework est robuste, riche en fonctionnalités et en développement actif, ce qui en fait un excellent choix. Il offre également une couche d'intégration C qui rend son utilisation facile avec notre code C de plateforme même si la plupart du framework est en C++. Ce tutoriel couvre comment commencer avec CppUTest sur vos propres projets.

Téléchargement de CppUTest

La page du projet CppUTest est disponible sur le site officiel, et le dépôt est sur github. Il est également inclus dans les référentiels de gestion des paquets pour de nombreuses distributions Linux, ainsi que homebrew sur Mac OS. Les exemples qui suivent ont été exécutés sur Mac OS X, mais ils sont dérivés du code écrit pour Red Hat, le système d'exploitation sur lequel notre plateforme fonctionne.

Les bases sont bien documentées sur la page d'accueil de CppUTest. Nous allons survoler cela et passer à certaines des fonctionnalités les plus intéressantes.

Poser les bases

Tout d'abord, écrivons du 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 organisé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 seront dans le répertoire t/.  La première chose à faire est de mettre en place un exécuteur de tests qui exécutera nos fichiers de test. C'est aussi 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);
}

Nous pouvons maintenant é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 en aurons besoin de deux : un pour les fichiers du projet sous src/, et un pour les tests.

Tout d'abord, écrivons du 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 organisé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 seront dans le répertoire t/.  La première chose à faire est de mettre en place un exécuteur de tests qui exécutera nos fichiers de test. C'est aussi 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);
}

Nous pouvons maintenant é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 en aurons besoin de deux : un pour les fichiers du projet sous src/, et un pour les tests.

Tout d'abord, écrivons du 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 organisé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 seront dans le répertoire t/.  La première chose à faire est de mettre en place un exécuteur de tests qui exécutera nos fichiers de test. C'est aussi 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);
}

Nous pouvons maintenant é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 en aurons besoin de deux : un pour les fichiers du projet sous src/, et un pour les tests.

Projet Makefile

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 vérifier 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

Tests Makefile

Pour les tests, les choses sont un peu plus compliquées car nous devons correctement charger et intégrer la bibliothèque CppUTest.

Le référentiel CppUTest fournit un fichier appelé « MakefileWorker.mk ». Il offre beaucoup de fonctionnalités qui facilitent 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 :

# we don’t want to use relative paths, so we set these variables
PROJECT_DIR=/path/to/project
SRC_DIR=$(PROJECT_DIR)/src
TEST_DIR=$(PROJECT_DIR)/t

# specify where the source code and includes are located
INCLUDE_DIRS=$(SRC_DIR)/code
SRC_DIRS=$(SRC_DIR)/code

# specify where the test code is located
TEST_SRC_DIRS=$(TEST_DIR)

# what to call the test binary
TEST_TARGET=example

# where the cpputest library is located
CPPUTEST_HOME=/usr/local

# run MakefileWorker.mk with the variables defined here
include MakefileWorker.mk

Notez que CPPUTEST_HOME doit être défini à l'endroit où CppUTest a été installé. Si vous avez installé un package de distribution, ce sera généralement sous /usr/local sur un système linux/mac. Si vous avez récupéré le référentiel vous-même, c'est là où se trouve ce checkout.

Toutes ces options sont documentées dans MakefileWorker.mk.

MakefileWorker.mk ajoute également quelques cibles de makefile, y compris les suivantes :

  1. all – construit les tests indiqués par le makefile

  2. clean – supprime tous les fichiers d'objet et gcov générés pour les tests

  3. realclean – supprime tous les fichiers d'objet ou gcov dans l'ensemble de l'arborescence de répertoires

  4. flags – énumère tous les drapeaux configurés utilisés pour compiler les tests

  5. debug – énumère tous les fichiers sources, objets, dépendances, et « choses à nettoyer »

Code Coverage

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 le cadre de la suite standard des utilitaires gcc. Cpputest s'intègre facilement avec gcov, tout ce que vous devez faire est d'ajouter cette ligne au makefile :

CPPUTEST_USE_GCOV=Y

Ensuite, nous devons nous assurer que le script filterGcov.sh de ce repo soit 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 le Makefile d'exemple, il serait déployé dans ‘/usr/local/scripts/filterGcov.sh’. Si vous exécutez CppUTest à partir d'un checkout de repo, 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
< compilation output >

for d in /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

for f in  ; 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

See gcov directory for details

Cela produira un certain nombre de fichiers dans un nouveau répertoire ‘gcov’. Ceux-ci sont :

  1. code.cpp.gcov – le fichier ‘gcov’ réel pour le code testé

  2. gcov_error.txt – un rapport d'erreur (dans notre cas, il devrait être vide)

  3. gcov_output.txt – la sortie réelle de la commande gcov qui a été exécutée

  4. gcov_report.txt – un résumé de la couverture pour chaque fichier testé

  5. gcov_report.txt.html – une version html du rapport gcov

Cpputest Détection de fuite de mémoire

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 enveloppes à la place. Cela lui permet de détecter rapidement les fuites et de les signaler pour chaque exécution de test. Ceci est activé par défaut dans MakefileWorker.mk, donc il est déjà en marche avec les étapes décrites jusqu'à présent.

Pour illustrer, faisons une fuite de mémoire dans test_func() !

Revenons à 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: error: Failure in TEST(AwesomeExamples, FirstExample)
Memory leak(s) found.
Alloc num (4)
Leak size: 1
Allocated at: ./code.c and line: 6
Type: "malloc"
Memory: <

Cela montre quel test a causé la fuite, où la fuite s'est produite dans le code source, et ce qui se trouvait dans la mémoire fuyante. Très utile!

Il y a quelques mises en garde avec cette fonctionnalité :

  1. Cpputest utilise des macros de préprocesseur pour redéfinir dynamiquement tous les appels aux fonctions de gestion de la mémoire standard. Cela signifie que cela ne fonctionnera que pour les appels dans le code source en test, car c'est ce qui est compilé avec les remplacements de CppUTest. Les fuites dans les bibliothèques liées ne seront pas détectées.

  2. Parfois, la mémoire qui est allouée pour toute la durée de vie du processus n'est pas destinée à être libérée. Cela peut générer de nombreuses erreurs spammantes 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 More ?

Ceci n'est que la partie émergée de l'iceberg en ce qui concerne toutes les fonctionnalités contenues dans cet outil. Outre les bases discutées ici, il comprend également un cadre de simulation, une couche d'intégration C directe et un cadre de plugin, pour n'en nommer que quelques-uns parmi les plus importants. Le repo contient également un répertoire complet de scripts d'assistance qui peuvent aider à automatiser certaines des parties routinières du travail avec le framework.

J'espère que les informations ici vous aideront à améliorer la qualité de votre code C/C++ avec cet excellent outil !

Autres news

Lire la suite de cette catégorie

A person is standing at a desk while typing on a laptop.

La plateforme native AI complète qui évolue avec votre business.

A person is standing at a desk while typing on a laptop.

La plateforme native AI complète qui évolue avec votre business.