FFF est un micro-travail pour créer de fausses fonctions C pour les tests. Parce que la vie est trop courte pour passer du temps à rédiger des fonctions de fausses fonctions pour les tests.
Pour exécuter tous les tests et exemples d'applications, appelez simplement $ buildandtest . Ce script s'appellera dans CMake avec les suivants:
cmake -B build -DFFF_GENERATE=ON -DFFF_UNIT_TESTING=ON
cmake --build build
ctest --test-dir build --output-on-failureDites que vous testez une interface utilisateur intégrée et que vous avez une fonction pour laquelle vous souhaitez créer un faux pour:
// UI.c
...
void DISPLAY_init ();
...Voici comment vous définissez une fausse fonction pour cela dans votre suite de tests:
// test.c(pp)
#include "fff.h"
DEFINE_FFF_GLOBALS ;
FAKE_VOID_FUNC ( DISPLAY_init );Et le test unitaire peut ressembler à ceci:
TEST_F ( GreeterTests , init_initialises_display )
{
UI_init ();
ASSERT_EQ ( DISPLAY_init_fake . call_count , 1 );
} Alors, que s'est-il passé ici? La première chose à noter est que le cadre est uniquement en en-tête, tout ce que vous avez à faire pour l'utiliser est de télécharger fff.h et de l'inclure dans votre suite de tests.
La magie est dans le FAKE_VOID_FUNC . Cela étend une macro qui définit une fonction renvoyant void qui n'a aucun argument. Il définit également une structure "function_name"_fake qui contient toutes les informations sur le faux. Par exemple, DISPLAY_init_fake.call_count est incrémenté chaque fois que la fonction de truqué est appelée.
Sous le capot, il génère une structure qui ressemble à ceci:
typedef struct DISPLAY_init_Fake {
unsigned int call_count ;
unsigned int arg_history_len ;
unsigned int arg_histories_dropped ;
void ( * custom_fake )();
} DISPLAY_init_Fake ;
DISPLAY_init_Fake DISPLAY_init_fake ;OK, assez avec les exemples de jouets. Qu'en est-il des fonctions de simulation avec des arguments?
// UI.c
...
void DISPLAY_output ( char * message );
...Voici comment vous définissez une fausse fonction pour cela dans votre suite de tests:
FAKE_VOID_FUNC ( DISPLAY_output , char * );Et le test unitaire peut ressembler à ceci:
TEST_F ( UITests , write_line_outputs_lines_to_display )
{
char msg [] = "helloworld" ;
UI_write_line ( msg );
ASSERT_EQ ( DISPLAY_output_fake . call_count , 1 );
ASSERT_EQ ( strncmp ( DISPLAY_output_fake . arg0_val , msg , 26 ), 0 );
} Il n'y a plus de magie ici, le FAKE_VOID_FUNC fonctionne comme dans l'exemple précédent. Le nombre d'arguments que prend la fonction est calculé et les arguments macro suivant le nom de la fonction définissent le type d'argument (un pointeur char dans cet exemple).
Une variable est créée pour chaque argument de la forme "function_name"fake.argN_val
Lorsque vous souhaitez définir une fausse fonction qui renvoie une valeur, vous devez utiliser la macro FAKE_VALUE_FUNC . Par exemple:
// UI.c
...
unsigned int DISPLAY_get_line_capacity ();
unsigned int DISPLAY_get_line_insert_index ();
...Voici comment vous définissez de fausses fonctions pour celles-ci dans votre suite de tests:
FAKE_VALUE_FUNC ( unsigned int , DISPLAY_get_line_capacity );
FAKE_VALUE_FUNC ( unsigned int , DISPLAY_get_line_insert_index );Et le test unitaire peut ressembler à ceci:
TEST_F ( UITests , when_empty_lines_write_line_doesnt_clear_screen )
{
// given
DISPLAY_get_line_insert_index_fake . return_val = 1 ;
char msg [] = "helloworld" ;
// when
UI_write_line ( msg );
// then
ASSERT_EQ ( DISPLAY_clear_fake . call_count , 0 );
}Bien sûr, vous pouvez mélanger et assortir ces macros pour définir une fonction de valeur avec des arguments, par exemple à simuler:
double pow ( double base , double exponent );Vous utiliseriez une syntaxe comme ceci:
FAKE_VALUE_FUNC ( double , pow , double , double );Les bons tests sont des tests isolés, il est donc important de réinitialiser les contrefaçons pour chaque test unitaire. Tous les contrefaçons ont une fonction de réinitialisation pour réinitialiser leurs arguments et comptes d'appels. Il est bonne pratique, c'est d'appeler la fonction de réinitialisation pour tous les contrefaçons de la fonction de configuration de votre suite de test.
void setup ()
{
// Register resets
RESET_FAKE ( DISPLAY_init );
RESET_FAKE ( DISPLAY_clear );
RESET_FAKE ( DISPLAY_output_message );
RESET_FAKE ( DISPLAY_get_line_capacity );
RESET_FAKE ( DISPLAY_get_line_insert_index );
}Vous voudrez peut-être définir une macro pour ce faire:
/* List of fakes used by this unit tester */
#define FFF_FAKES_LIST ( FAKE )
FAKE(DISPLAY_init)
FAKE(DISPLAY_clear)
FAKE(DISPLAY_output_message)
FAKE(DISPLAY_get_line_capacity)
FAKE(DISPLAY_get_line_insert_index)
void setup ()
{
/* Register resets */
FFF_FAKES_LIST ( RESET_FAKE );
/* reset common FFF internal structures */
FFF_RESET_HISTORY ();
}Dites que vous voulez tester qu'une fonction appelle Functiona, puis FonctionB, puis fonctiona à nouveau, comment feriez-vous cela? Eh bien, FFF maintient un historique d'appel afin qu'il soit facile d'affirmer ces attentes.
Voici comment cela fonctionne:
FAKE_VOID_FUNC ( voidfunc2 , char , char );
FAKE_VALUE_FUNC ( long , longfunc0 );
TEST_F ( FFFTestSuite , calls_in_correct_order )
{
longfunc0 ();
voidfunc2 ();
longfunc0 ();
ASSERT_EQ ( fff . call_history [ 0 ], ( void * ) longfunc0 );
ASSERT_EQ ( fff . call_history [ 1 ], ( void * ) voidfunc2 );
ASSERT_EQ ( fff . call_history [ 2 ], ( void * ) longfunc0 );
} Ils sont réinitialisés en appelant FFF_RESET_HISTORY();
Le cadre stockera par défaut les arguments pour les dix derniers appels passés à une fausse fonction.
TEST_F ( FFFTestSuite , when_fake_func_called_then_arguments_captured_in_history )
{
voidfunc2 ( 'g' , 'h' );
voidfunc2 ( 'i' , 'j' );
ASSERT_EQ ( 'g' , voidfunc2_fake . arg0_history [ 0 ]);
ASSERT_EQ ( 'h' , voidfunc2_fake . arg1_history [ 0 ]);
ASSERT_EQ ( 'i' , voidfunc2_fake . arg0_history [ 1 ]);
ASSERT_EQ ( 'j' , voidfunc2_fake . arg1_history [ 1 ]);
}Il existe deux façons de savoir si les appels ont été supprimés. La première consiste à vérifier le compteur d'histoires supprimées:
TEST_F ( FFFTestSuite , when_fake_func_called_max_times_plus_one_then_one_argument_history_dropped )
{
int i ;
for ( i = 0 ; i < 10 ; i ++ )
{
voidfunc2 ( '1' + i , '2' + i );
}
voidfunc2 ( '1' , '2' );
ASSERT_EQ ( 1u , voidfunc2_fake . arg_histories_dropped );
}L'autre consiste à vérifier si le nombre d'appels est supérieur à la taille de l'historique:
ASSERT ( voidfunc2_fake . arg_history_len < voidfunc2_fake . call_count ); Les histoires d'argument pour une fausse fonction sont réinitialisées lorsque la fonction RESET_FAKE est appelée
Si vous souhaitez contrôler le nombre d'appels à capturer pour l'historique des arguments, vous pouvez remplacer la valeur par défaut en la définissant avant d'inclure le fff.h comme ceci:
// Want to keep the argument history for 13 calls
#define FFF_ARG_HISTORY_LEN 13
// Want to keep the call sequence history for 17 function calls
#define FFF_CALL_HISTORY_LEN 17
#include "../fff.h" Souvent, dans les tests, nous aimerions tester le comportement de la séquence des événements d'appel de fonction. Une façon de le faire avec FFF est de spécifier une séquence de valeurs de retour avec la fausse fonction. Il est probablement plus facile à décrire avec un exemple:
// faking "long longfunc();"
FAKE_VALUE_FUNC ( long , longfunc0 );
TEST_F ( FFFTestSuite , return_value_sequences_exhausted )
{
long myReturnVals [ 3 ] = { 3 , 7 , 9 };
SET_RETURN_SEQ ( longfunc0 , myReturnVals , 3 );
ASSERT_EQ ( myReturnVals [ 0 ], longfunc0 ());
ASSERT_EQ ( myReturnVals [ 1 ], longfunc0 ());
ASSERT_EQ ( myReturnVals [ 2 ], longfunc0 ());
ASSERT_EQ ( myReturnVals [ 2 ], longfunc0 ());
ASSERT_EQ ( myReturnVals [ 2 ], longfunc0 ());
} En spécifiant une séquence de valeur de retour à l'aide de la macro SET_RETURN_SEQ , le faux renvoie les valeurs données dans le tableau de paramètres en séquence. Lorsque la fin de la séquence est atteinte, le faux continuera à renvoyer la dernière valeur de la séquence indéfiniment.
Vous pouvez spécifier votre propre fonction pour fournir la valeur de retour du faux. Cela se fait en définissant le membre custom_fake du faux. Voici un exemple:
#define MEANING_OF_LIFE 42
long my_custom_value_fake ( void )
{
return MEANING_OF_LIFE ;
}
TEST_F ( FFFTestSuite , when_value_custom_fake_called_THEN_it_returns_custom_return_value )
{
longfunc0_fake . custom_fake = my_custom_value_fake ;
long retval = longfunc0 ();
ASSERT_EQ ( MEANING_OF_LIFE , retval );
} Supposons que vous ayez une fonction avec un paramètre OUT, et que vous souhaitez qu'il ait un comportement différent sur les trois premiers appels, par exemple: définissez la valeur «x» sur le paramètre OUT sur le premier appel, la valeur «Y» sur le paramètre OUT sur le deuxième appel et la valeur «z» au paramètre OUT sur le troisième appel. Vous pouvez spécifier une séquence de fonctions personnalisées sur une fonction non variadique à l'aide de la macro SET_CUSTOM_FAKE_SEQ . Voici un exemple:
void voidfunc1outparam_custom_fake1 ( char * a )
{
* a = 'x' ;
}
void voidfunc1outparam_custom_fake2 ( char * a )
{
* a = 'y' ;
}
void voidfunc1outparam_custom_fake3 ( char * a )
{
* a = 'z' ;
}
TEST_F ( FFFTestSuite , custom_fake_sequence_not_exausthed )
{
void ( * custom_fakes [])( char * ) = { voidfunc1outparam_custom_fake1 ,
voidfunc1outparam_custom_fake2 ,
voidfunc1outparam_custom_fake3 };
char a = 'a' ;
SET_CUSTOM_FAKE_SEQ ( voidfunc1outparam , custom_fakes , 3 );
voidfunc1outparam ( & a );
ASSERT_EQ ( 'x' , a );
voidfunc1outparam ( & a );
ASSERT_EQ ( 'y' , a );
voidfunc1outparam ( & a );
ASSERT_EQ ( 'z' , a );
} Le faux appellera vos fonctions personnalisées dans l'ordre spécifié par la macro SET_CUSTOM_FAKE_SEQ . Lorsque le dernier faux personnalisé est atteint, le faux continuera d'appeler le dernier faux personnalisé dans la séquence. Cette macro fonctionne un peu comme la macro SET_RETURN_SEQ .
Dites que vous avez deux fonctions F1 et F2. F2 doit être appelé pour publier certaines ressources allouées par F1, mais uniquement dans les cas où F1 renvoie zéro. F1 pourrait être pthread_mutex_trylock et f2 pourrait être pthread_mutex_unlock. FFF sauvera l'historique des valeurs renvoyées afin que cela puisse être facilement vérifié, même lorsque vous utilisez une séquence de contrefaçons personnalisées. Voici un exemple simple:
TEST_F(FFFTestSuite, return_value_sequence_saved_in_history)
{
long myReturnVals[3] = { 3, 7, 9 };
SET_RETURN_SEQ(longfunc0, myReturnVals, 3);
longfunc0();
longfunc0();
longfunc0();
ASSERT_EQ(myReturnVals[0], longfunc0_fake.return_val_history[0]);
ASSERT_EQ(myReturnVals[1], longfunc0_fake.return_val_history[1]);
ASSERT_EQ(myReturnVals[2], longfunc0_fake.return_val_history[2]);
}
Vous accédez aux valeurs retournées dans le champ return_val_history .
Vous pouvez simuler les fonctions variadiques à l'aide des macros FAKE_VALUE_FUNC_VARARG et FAKE_VOID_FUNC_VARARG . Par exemple:
FAKE_VALUE_FUNC_VARARG(int, fprintf, FILE *, const char*, ...);
Afin d'accéder aux paramètres variadiques à partir d'une fausse fonction personnalisée, déclarez un paramètre va_list . Par exemple, un faux personnalisé pour fprintf() pourrait appeler le vrai fprintf() comme ceci:
int fprintf_custom(FILE *stream, const char *format, va_list ap) {
if (fprintf0_fake.return_val < 0) // should we fail?
return fprintf0_fake.return_val;
return vfprintf(stream, format, ap);
}
Tout comme les délégués de valeur de retour, vous pouvez également spécifier des séquences pour les fonctions variadiques à l'aide de SET_CUSTOM_FAKE_SEQ . Voir les fichiers de test pour des exemples.
FFF a une capacité limitée pour activer les spécifications des conventions d'appel visuelles C / C ++ visuelles de Microsoft, mais cette prise en charge doit être activée lors de la génération du fichier d'en-tête de FFF fff.h
ruby fakegen.rb --with-calling-conventions > fff.h En permettant ce support, tous les faux échafaudages de la fonction de FFF nécessiteront la spécification d'une convention d'appel, par exemple __cdecl pour chaque valeur ou faux.
Voici quelques exemples de base: notez que le placement de la convention d'appel spécifiée est différent selon que le faux est une fonction vide ou de valeur.
FAKE_VOID_FUNC ( __cdecl , voidfunc1 , int );
FAKE_VALUE_FUNC ( long , __cdecl , longfunc0 );Le mécanisme de base que FFF vous fournit dans ce cas est le champ Custom_fake décrit dans l'exemple de délégué de valeur de retour personnalisé ci-dessus.
Vous devez créer une fonction personnalisée (par exemple gettime_custom_fake) pour produire la sortie éventuellement en utilisant une variable d'assistance (par exemple gettime_custom_now) pour récupérer cette sortie. Ensuite, une créativité pour tout attacher ensemble. La partie la plus importante (IMHO) est de garder votre cas de test lisible et maintenable.
Dans le cas où votre projet utilise un compilateur C qui prend en charge les fonctions imbriquées (par exemple GCC), ou lorsque vous utilisez C ++ Lambdas, vous pouvez même combiner tout cela dans une fonction de test unitaire unique afin que vous puissiez facilement superviser tous les détails du test.
#include <functional>
/* Configure FFF to use std::function, which enables capturing lambdas */
#define CUSTOM_FFF_FUNCTION_TEMPLATE ( RETURN , FUNCNAME , ...)
std::function<RETURN (__VA_ARGS__)> FUNCNAME
#include "fff.h"
/* The time structure */
typedef struct {
int hour , min ;
} Time ;
/* Our fake function */
FAKE_VOID_FUNC ( getTime , Time * );
/* A test using the getTime fake function */
TEST_F ( FFFTestSuite , when_value_custom_fake_called_THEN_it_returns_custom_output )
{
Time t ;
Time getTime_custom_now = {
. hour = 13 ,
. min = 05 ,
};
getTime_fake . custom_fake = [ getTime_custom_now ]( Time * now ) {
* now = getTime_custom_now ;
};
/* when getTime is called */
getTime ( & t );
/* then the specific time must be produced */
ASSERT_EQ ( t . hour , 13 );
ASSERT_EQ ( t . min , 05 );
}L'utilisation de fonctions FFF pour stub a un paramètre de pointeur de fonction peut causer des problèmes lorsque vous essayez de les étouffer. Ici est un exemple de la façon de gérer cette situation.
Si vous avez besoin de coller une fonction qui a un paramètre de pointeur de fonction, par exemple quelque chose comme:
/* timer.h */
typedef int timer_handle ;
extern int timer_start ( timer_handle handle , long delay , void ( * cb_function ) ( int arg ), int arg ); Ensuite, la création d'un faux comme ci-dessous échouera horriblement lorsque vous essayez de compiler car la macro FFF se développera en interne dans une variable illégale int (*)(int) arg2_val .
/* The fake, attempt one */
FAKE_VALUE_FUNC ( int ,
timer_start ,
timer_handle ,
long ,
void ( * ) ( int argument ),
int );La solution à ce problème est de créer un type de pontage qui n'a qu'à être visible dans le testeur unitaire. Le faux utilisera ce type intermédiaire. De cette façon, le compilateur ne se plaindra pas car les types correspondent.
/* Additional type needed to be able to use callback in fff */
typedef void ( * timer_cb ) ( int argument );
/* The fake, attempt two */
FAKE_VALUE_FUNC ( int ,
timer_start ,
timer_handle ,
long ,
timer_cb ,
int );Voici quelques idées sur la création d'un cas de test avec des rappels.
/* Unit test */
TEST_F ( FFFTestSuite , test_fake_with_function_pointer )
{
int cb_timeout_called = 0 ;
int result = 0 ;
void cb_timeout ( int argument )
{
cb_timeout_called ++ ;
}
int timer_start_custom_fake ( timer_handle handle ,
long delay ,
void ( * cb_function ) ( int arg ),
int arg )
{
if ( cb_function ) cb_function ( arg );
return timer_start_fake . return_val ;
}
/* given the custom fake for timer_start */
timer_start_fake . return_val = 33 ;
timer_start_fake . custom_fake = timer_start_custom_fake ;
/* when timer_start is called
* (actually you would call your own function-under-test
* that would then call the fake function)
*/
result = timer_start ( 10 , 100 , cb_timeout , 55 );
/* then the timer_start fake must have been called correctly */
ASSERT_EQ ( result , 33 );
ASSERT_EQ ( timer_start_fake . call_count , 1 );
ASSERT_EQ ( timer_start_fake . arg0_val , 10 );
ASSERT_EQ ( timer_start_fake . arg1_val , 100 );
ASSERT_EQ ( timer_start_fake . arg2_val , cb_timeout ); /* callback provided by unit tester */
ASSERT_EQ ( timer_start_fake . arg3_val , 55 );
/* and ofcourse our custom fake correctly calls the registered callback */
ASSERT_EQ ( cb_timeout_called , 1 );
} Les fonctions FFF comme FAKE_VALUE_FUNC effectueront à la fois la déclaration et la définition de la fausse fonction et les structures de données correspondantes. Cela ne peut pas être placé dans un en-tête, car cela conduira à plusieurs définitions des fausses fonctions.
La solution consiste à séparer la déclaration et la définition des contrefaçons et de placer la déclaration dans un fichier d'en-tête public et la définition dans un fichier source privé.
Voici un exemple de la façon dont cela pourrait être fait:
/* Public header file */
#include "fff.h"
DECLARE_FAKE_VALUE_FUNC ( int , value_function , int , int );
DECLARE_FAKE_VOID_FUNC ( void_function , int , int );
DECLARE_FAKE_VALUE_FUNC_VARARG ( int , value_function_vargs , const char * , int , ...);
DECLARE_FAKE_VOID_FUNC_VARARG ( void_function_vargs , const char * , int , ...);
/* Private source file file */
#include "public_header.h"
DEFINE_FAKE_VALUE_FUNC ( int , value_function , int , int );
DEFINE_FAKE_VOID_FUNC ( void_function , int , int );
DEFINE_FAKE_VALUE_FUNC_VARARG ( int , value_function_vargs , const char * , int , ...);
DEFINE_FAKE_VOID_FUNC_VARARG ( void_function_vargs , const char * , int , ...); Vous pouvez spécifier les attributs de fonction GCC pour vos contrefaçons à l'aide de la directive FFF_GCC_FUNCTION_ATTRIBUTES .
Un attribut utile est l'attribut faible qui marque une fonction de telle sorte qu'il peut être remplacé par une variante non weak au moment du lien. L'utilisation de fonctions faibles en combinaison avec FFF peut aider à simplifier votre approche de test.
Par exemple:
Vous pouvez marquer tous les contrefaçons avec l'attribut faible comme ainsi:
#define FFF_GCC_FUNCTION_ATTRIBUTES __attribute__((weak))
#include "fff.h"
Voir l'exemple de projet qui démontre l'approche ci-dessus: ./Examples/weak_linking .
Regardez dans le répertoire des exemples pour des exemples de longueur complète en C et C ++. Il existe également une suite de tests pour le cadre dans le répertoire de test.
Alors, quel est le point?
| Macro | Description | Exemple |
|---|---|---|
| FALSE_VOID_FUNC (fn [, arg_types *]); | Définissez une fausse fonction nommée FN renvoyant vide avec n arguments | FALSE_VOID_FUNC (Display_Output_Message, const char *); |
| FALSE_VALUE_FUNC (return_type, fn [, arg_types *]); | Définissez une fausse fonction renvoyant une valeur avec le type return_type prenant n arguments | FALSE_VALUE_FUNC (int, display_get_line_insert_index); |
| FALSE_VOID_FUNC_VARARG (fn [, arg_types *], ...); | Définissez une fausse fonction variatique renvoyant vide avec le type return_type prenant n arguments et n arguments variadiques | FALSE_VOID_FUNC_VARARG (FN, const char *, ...) |
| Faux_value_func_vararg (return_type, fn [, arg_types *], ...); | Définissez une fausse fonction variatique renvoyant une valeur avec le type return_type prenant n arguments et n arguments variadiques | Faux_value_func_vararg (int, fprintf, fichier *, const char *, ...) |
| Reset_fake (fn); | Réinitialisez l'état de la fausse fonction appelée FN | Reset_fake (display_init); |