Accueil : https://github.com/mity/acutest
Acutest est une installation de tests unitaires C/C++ visant à être aussi simple que possible, à ne pas gêner le développeur et à minimiser les dépendances externes.
Pour y parvenir, l’implémentation complète réside dans un seul fichier d’en-tête C, et son noyau ne dépend que de quelques fonctions standard de la bibliothèque C.
Principales caractéristiques :
acutest.h
.main()
).--tap
).--xml-output=FILE
).Fonctionnalités spécifiques au C++ :
std::exception
, what()
est écrit dans le message d'erreur.Fonctionnalités spécifiques Unix/Posix :
clock_gettime()
), l'utilisateur peut mesurer les temps d'exécution des tests avec --time=real
(identique à --time
) et --time=cpu
.Fonctionnalités spécifiques à Linux :
Fonctionnalités spécifiques à Windows :
--time
.Fonctionnalités spécifiques à macOS :
Tout module C/C++ implémentant un ou plusieurs tests unitaires et incluant acutest.h
, peut être construit en tant que programme autonome. Nous appelons le binaire obtenu une « suite de tests » pour les besoins de ce document. La suite est ensuite exécutée pour exécuter les tests, comme spécifié avec ses options de ligne de commande.
Nous disons que tout test unitaire réussit si et seulement si :
Pour utiliser Acutest, incluez simplement le fichier d'en-tête acutest.h
au début du fichier source C/C++ implémentant un ou plusieurs tests unitaires. Notez que l'en-tête fournit l'implémentation de la fonction main()
.
#include "acutest.h"
Chaque test est censé être implémenté en fonction avec le prototype suivant :
void test_example ( void );
Les tests peuvent utiliser certaines macros de préprocesseur pour valider les conditions de test. Ils peuvent être utilisés plusieurs fois, et si l’une de ces conditions échoue, le test concerné est considéré comme un échec.
TEST_CHECK
est la macro de test la plus couramment utilisée qui teste simplement une condition booléenne et échoue si la condition est évaluée comme fausse (ou zéro).
Par exemple:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_CHECK ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_CHECK ( mem != NULL );
}
Notez que les tests doivent être complètement indépendants les uns des autres. Chaque fois que la suite de tests est invoquée, l'utilisateur peut exécuter n'importe quel nombre de tests dans la suite, dans n'importe quel ordre. De plus, par défaut, sur les plates-formes prises en charge, chaque test unitaire est exécuté en tant que (sous)processus autonome.
Enfin, le fichier source de la suite de tests doit lister les tests unitaires, à l'aide de la macro TEST_LIST
. La liste spécifie le nom de chaque test (il doit être unique) et le pointeur vers une fonction implémentant le test. Je recommande des noms faciles à utiliser en ligne de commande : évitez surtout les espaces et autres caractères spéciaux qui pourraient nécessiter un échappement dans le shell ; évitez également le tiret ( -
) comme premier caractère du nom, car il pourrait alors être interprété comme une option de ligne de commande et non comme un nom de test.
TEST_LIST = {
{ "example" , test_example },
...
{ NULL, NULL } /* zeroed record marking the end of the list */
};
Notez que la liste de tests doit se terminer par un enregistrement mis à zéro.
Pour une suite de tests de base, c'est plus ou moins tout ce que vous devez savoir. Cependant Acutest fournit quelques macros supplémentaires qui peuvent être utiles dans certaines situations spécifiques. Nous les abordons dans les sous-sections suivantes.
Il existe une macro TEST_ASSERT
qui est très similaire à TEST_CHECK
mais, si elle échoue, elle abandonne instantanément l'exécution du test unitaire en cours.
Par exemple:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_ASSERT ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_ASSERT ( mem != NULL );
}
L'avortement en cas d'échec s'effectue soit en appelant abort()
(si le test est exécuté en tant que processus enfant), soit via longjmp()
(s'il ne l'est pas).
Par conséquent, il ne doit être utilisé que si vous comprenez les coûts liés à un avortement aussi brutal du test. En fonction de ce que fait votre test unitaire, il peut inclure des descripteurs de fichiers non vidés, des fuites de mémoire, des objets C++ détruits sans que leurs destructeurs ne soient appelés, etc.
En général, TEST_CHECK
doit être préféré à TEST_ASSERT
, sauf si vous savez exactement ce que vous faites et pourquoi vous avez choisi TEST_ASSERT
dans une situation particulière.
Pour C++, il existe une macro supplémentaire TEST_EXCEPTION
pour vérifier que le code donné (généralement juste une fonction ou un appel de méthode) lève le type d'exception attendu.
La vérification échoue si la fonction ne lève aucune exception ou si elle renvoie quelque chose d'incompatible.
Par exemple:
void test_example ( void )
{
TEST_EXCEPTION ( CallSomeFunction (), std:: exception );
}
Si une vérification de condition échoue, il est souvent utile de fournir des informations supplémentaires sur la situation afin que le problème soit plus facile à déboguer. Acutest fournit à cet effet les macros TEST_MSG
et TEST_DUMP
.
Le premier génère n'importe quel message de type printf
, l'autre génère un vidage hexadécimal d'un bloc de mémoire fourni.
Ainsi par exemple :
void test_example ( void )
{
char produced [ 100 ];
char expected [] = "Hello World!" ;
SomeSprintfLikeFunction ( produced , "Hello %s!" , "world" );
TEST_CHECK ( strcmp ( produced , expected ) == 0 );
TEST_MSG ( "Expected: %s" , expected );
TEST_MSG ( "Produced: %s" , produced );
/* Or, if the function could provide some binary stuff, we might rather
* use TEST_DUMP instead in order to output a hexadecimal dump of the data.
*/
TEST_DUMP ( "Expected:" , expected , strlen ( expected ));
TEST_DUMP ( "Produced:" , produced , strlen ( produced ));
}
Notez que les deux macros génèrent quoi que ce soit uniquement lorsque la macro de vérification la plus récente a échoué. Autrement dit, ces deux éléments sont équivalents :
if (! TEST_CHECK ( some_condition != 0 ))
TEST_MSG ( "some message" );
TEST_CHECK ( some_condition != 0 );
TEST_MSG ( "some message" );
(Notez que TEST_MSG
nécessite le compilateur avec prise en charge des macros variadiques.)
Parfois, il est utile de concevoir votre fonction de test comme une boucle sur des données fournissant une collection de vecteurs de test et leurs résultats attendus respectifs. Par exemple, imaginez que notre test unitaire soit censé vérifier une sorte d'implémentation de fonction de hachage et que nous ayons une collection de vecteurs de test pour cela dans la spécification de hachage.
Dans de tels cas, il est très utile d'obtenir un nom associé à chaque vecteur de test et d'afficher le nom dans le journal de sortie afin que si une vérification échoue, il soit facile d'identifier le vecteur de test coupable. Cependant, le corps de la boucle peut exécuter des dizaines de macros de vérification et il peut donc s'avérer peu pratique d'ajouter un tel nom pour personnaliser chaque message de vérification de la boucle.
Pour résoudre ce problème, Acutest fournit la macro TEST_CASE
. La macro spécifie une chaîne servant de nom de vecteur de test. Lorsqu'il est utilisé, Acutest s'assure que dans le journal de sortie, le nom fourni précède tout message provenant des vérifications de condition ultérieures.
Par exemple, supposons que nous testions SomeFunction()
qui est censé, pour un tableau d'octets donné d'une certaine taille, renvoyer un autre tableau d'octets dans un tampon nouvellement malloc
-ed. Ensuite, nous pouvons faire quelque chose comme ceci :
struct TestVector {
const char * name ;
const uint8_t * input ;
size_t input_size ;
const uint8_t * expected_output ;
size_t expected_output_size ;
};
struct TestVector test_vectors [] = {
/* some data */
};
void test_example ( void )
{
int i ;
const uint8_t * output ;
size_t output_size ;
for ( i = 0 ; i < sizeof ( test_vectors ) / sizeof ( test_vectors [ 0 ]); i ++ ) {
struct TestVector * vec = & test_vectors [ i ];
/* Output the name of the tested test vector. */
TEST_CASE ( vec . name );
/* Now, we can check the function produces what it should for the
* current test vector. If any of the following checking macros
* produces any output (either because the check fails, or because
* high `--verbose` level is used), Acutest also outputs the currently
* tested vector's name. */
output = SomeFunction ( vec -> input , vec -> input_size , & output_size );
if ( TEST_CHECK ( output != NULL )) {
TEST_CHECK ( output_size == vec -> expected_output_size );
TEST_CHECK ( memcmp ( output , vec -> expected_output , output_size ) == 0 );
free ( output );
}
}
}
Le nom spécifié s'applique à tous les contrôles exécutés après l'utilisation de TEST_CASE
TEST_CASE
soit à nouveau utilisé pour spécifier un autre nom ; ouTEST_CASE
avec NULL
comme argument.La plupart des macros mentionnées dans les sections précédentes ont une contrepartie qui permet d'afficher des messages personnalisés au lieu de certains messages par défaut.
Toutes ces éléments portent le même nom que les macros susmentionnées, avec simplement le suffixe de trait de soulignement ajouté. Avec le suffixe, ils attendent alors un format de chaîne de type printf
et les arguments supplémentaires correspondants.
Ainsi, par exemple, au lieu des simples macros de vérification
TEST_CHECK (a == b);
TEST_ASSERT (x < y);
TEST_EXCEPTION (SomeFunction(), std::exception);
nous pouvons utiliser leurs homologues respectifs avec des messages personnalisés :
TEST_CHECK_ (a == b, " %d is equal to %d " , a, b);
TEST_ASSERT_ (x < y, " %d is lower than %d " , x, y);
TEST_EXCEPTION_ (SomeFunction(), std::exception, "SomeFunction() throws std::exception");
Vous devez utiliser une formulation neutre pour eux car, avec l'option de ligne de commande --verbose
, les messages sont déconnectés même si la vérification correspondante réussit.
(Si vous avez besoin de générer des informations de diagnostic juste en cas d'échec de la vérification, utilisez la macro TEST_MSG
. C'est exactement son objectif.)
De même, au lieu de
TEST_CASE ( "name" );
nous pouvons utiliser plus riche
TEST_CASE_ ( "iteration #%d" , 42 );
Notez cependant que tous ces éléments ne peuvent être utilisés que si votre compilateur prend en charge les macros de préprocesseur variadiques. Les macros variadiques sont devenues un élément standard du langage C avec C99.
Lorsque nous avons terminé d’implémenter les tests, nous pouvons simplement les compiler comme un simple programme C/C++. Par exemple, en supposant que cc
soit votre compilateur C :
$ cc test_example.c -o test_example
Lorsque la suite de tests est compilée, le binaire de test obtenu peut être utilisé pour exécuter les tests.
Le code de sortie de la suite de tests est 0 si tous les tests unitaires exécutés réussissent, 1 si l'un d'entre eux échoue, ou tout autre nombre si une erreur interne se produit.
Par défaut (sans aucune option de ligne de commande), il exécute tous les tests unitaires implémentés. Il peut également exécuter uniquement un sous-ensemble des tests unitaires comme spécifié sur la ligne de commande :
$ ./test_example # Runs all tests in the suite
$ ./test_example test1 test2 # Runs only tests specified
$ ./test_example --exclude test3 # Runs all tests but those specified
Notez qu'un seul argument de ligne de commande peut sélectionner tout un groupe d'unités de test car Acutest implémente plusieurs niveaux de sélection de tests unitaires (le 1er correspondant à au moins une unité de test est utilisé) :
Correspondance exacte : lorsque l'argument correspond exactement au nom complet d'un test unitaire, seul le test donné est sélectionné.
Correspondance de mots : lorsque l'argument ne correspond à aucun nom de test complet, mais qu'il correspond à un mot entier dans un ou plusieurs noms de tests, alors tous ces tests sont sélectionnés.
Les caractères suivants sont reconnus comme délimiteurs de mots : espace
, tabulateur t
, tiret -
, trait de soulignement _
, barre oblique /
, point .
, virgule ,
, deux-points :
, point-virgule ;
.
Correspondance de sous-chaîne : si même la correspondance de mots n'a réussi à sélectionner aucun test, alors tous les tests dont le nom contient l'argument comme sous-chaîne sont sélectionnés.
En adoptant une stratégie de dénomination de test appropriée, cela permet à l'utilisateur d'exécuter (ou d'ignorer si --exclude
est utilisé) facilement toute une famille de tests associés avec un seul argument de ligne de commande.
Par exemple, considérons la suite de tests test_example
qui implémente les tests foo-1
, foo-2
, foomatic
, bar-1
et bar-10
:
$ ./test_example bar-1 # Runs only the test 'bar-1' (exact match)
$ ./test_example foo # Runs 'foo-1' and 'foo-2' (word match)
$ ./test_example oo # Runs 'foo-1', 'foo-2' and 'foomatic' (substring match)
$ ./test_example 1 # Runs 'foo-1' and 'bar-1' (word match)
Vous pouvez utiliser --list
ou -l
pour simplement lister tous les tests unitaires implémentés par la suite de tests donnée :
$ ./test_example --list
Pour voir la description de toutes les options de ligne de commande prises en charge, exécutez le binaire avec l'option --help
:
$ ./test_example --help
Q : Ce projet n'était-il pas connu sous le nom de « CUTest » ?
R : Oui. Il a été renommé car le nom d'origine s'est avéré trop surchargé.
Q : Dois-je distribuer le fichier README.md
et/ou LICENSE.md
?
R : Non. L'en-tête acutest.h
comprend l'URL de notre dépôt, la note de droit d'auteur et les termes de la licence MIT qu'il contient. Tant que vous les laissez intacts, tout va bien si vous ajoutez uniquement l'en-tête dans votre projet. Après tout, sa simplicité d’utilisation et sa nature d’en-tête tout-en-un sont notre objectif principal.
Acutest est couvert par une licence MIT, voir le fichier LICENSE.md
ou le début de acutest.h
pour son texte intégral.
Le projet réside sur github :
Vous pouvez y trouver la dernière version d'Acutest, contribuer à des améliorations ou signaler des bugs.