FFF es un micro-marco para crear funciones de C falsas para las pruebas. Porque la vida es demasiado corta para pasar el tiempo de escritura a mano funciones falsas para las pruebas.
Para ejecutar todas las pruebas y aplicaciones de muestra, simplemente llame $ buildandtest . Este script llamará a Cmake con lo siguiente:
cmake -B build -DFFF_GENERATE=ON -DFFF_UNIT_TESTING=ON
cmake --build build
ctest --test-dir build --output-on-failureDigamos que está probando una interfaz de usuario integrada y tiene una función que desea crear una falsificación:
// UI.c
...
void DISPLAY_init ();
...Así es como definiría una función falsa para esto en su suite de prueba:
// test.c(pp)
#include "fff.h"
DEFINE_FFF_GLOBALS ;
FAKE_VOID_FUNC ( DISPLAY_init );Y la prueba unitaria podría verse algo así:
TEST_F ( GreeterTests , init_initialises_display )
{
UI_init ();
ASSERT_EQ ( DISPLAY_init_fake . call_count , 1 );
} Entonces, ¿qué ha pasado aquí? Lo primero que debe tener en cuenta es que el marco es solo encabezado, todo lo que necesita hacer para usarlo es descargar fff.h e incluirlo en su suite de prueba.
La magia está en el FAKE_VOID_FUNC . Esto expande una macro que define una función que devuelve void que tiene cero argumentos. También define una estructura "function_name"_fake que contiene toda la información sobre la falsa. Por ejemplo, DISPLAY_init_fake.call_count se incrementa cada vez que se llama a la función falsa.
Debajo del capó genera una estructura que se ve así:
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, suficiente con los ejemplos de juguetes. ¿Qué hay de fingir funciones con argumentos?
// UI.c
...
void DISPLAY_output ( char * message );
...Así es como definiría una función falsa para esto en su suite de prueba:
FAKE_VOID_FUNC ( DISPLAY_output , char * );Y la prueba unitaria podría verse algo así:
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 );
} No hay más magia aquí, el FAKE_VOID_FUNC funciona como en el ejemplo anterior. El número de argumentos que toma la función se calcula, y los argumentos macro que siguen el nombre de la función define el tipo de argumento (un puntero de char en este ejemplo).
Se crea una variable para cada argumento en el formulario "function_name"fake.argN_val
Cuando desee definir una función falsa que devuelva un valor, debe usar la macro FAKE_VALUE_FUNC . Por ejemplo:
// UI.c
...
unsigned int DISPLAY_get_line_capacity ();
unsigned int DISPLAY_get_line_insert_index ();
...Así es como definirías las funciones falsas para estas en tu suite de prueba:
FAKE_VALUE_FUNC ( unsigned int , DISPLAY_get_line_capacity );
FAKE_VALUE_FUNC ( unsigned int , DISPLAY_get_line_insert_index );Y la prueba unitaria podría verse algo así:
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 );
}Por supuesto, puede mezclar y combinar estas macros para definir una función de valor con los argumentos, por ejemplo, para fingir:
double pow ( double base , double exponent );Usarías una sintaxis como esta:
FAKE_VALUE_FUNC ( double , pow , double , double );Las buenas pruebas son pruebas aisladas, por lo que es importante restablecer las falsificaciones para cada prueba unitaria. Todas las falsificaciones tienen una función de reinicio para restablecer sus argumentos y recuentos de llamadas. Es una buena práctica llamar a la función de reinicio para todas las falsificaciones en la función de configuración de su conjunto de pruebas.
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 );
}Es posible que desee definir una macro para hacer esto:
/* 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 ();
}Digamos que desea probar que una función llama a funciones, luego funcioneb, luego funcione nuevamente, ¿cómo lo haría? Bueno, FFF mantiene un historial de llamadas para que sea fácil afirmar estas expectativas.
Así es como funciona:
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 );
} Se restablecen llamando FFF_RESET_HISTORY();
El marco almacenará por defecto los argumentos para las últimas diez llamadas realizadas a una función falsa.
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 ]);
}Hay dos formas de averiguar si se han eliminado las llamadas. El primero es verificar el contador de historias caídas:
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 );
}El otro es verificar si el recuento de llamadas es mayor que el tamaño del historial:
ASSERT ( voidfunc2_fake . arg_history_len < voidfunc2_fake . call_count ); Las historias de argumentos para una función falsa se restablecen cuando se llama la función RESET_FAKE
Si desea controlar cuántas llamadas para capturar para el historial de argumentos, puede anular el valor predeterminado, definirlo antes, incluir el fff.h así:
// 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" A menudo en las pruebas nos gustaría probar el comportamiento de la secuencia de eventos de llamadas de funciones. Una forma de hacer esto con FFF es especificar una secuencia de valores de retorno con la función falsa. Probablemente sea más fácil de describir con un ejemplo:
// 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 ());
} Al especificar una secuencia de valor de retorno utilizando la macro SET_RETURN_SEQ , el falso devolverá los valores dados en la matriz de parámetros en secuencia. Cuando llegue al final de la secuencia, el falso continuará devolviendo el último valor en la secuencia indefinidamente.
Puede especificar su propia función para proporcionar el valor de retorno para el falso. Esto se hace configurando el miembro custom_fake de la falsa. Aquí hay un ejemplo:
#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 );
} Digamos que tiene una función con un parámetro de salida, y desea que tenga un comportamiento diferente en las tres primeras llamadas, por ejemplo: configure el valor 'x' en el parámetro de salida en la primera llamada, el valor 'y' al parámetro de salida en la segunda llamada y el valor 'z' en el parámetro de salida en la tercera llamada. Puede especificar una secuencia de funciones personalizadas a una función no variádica utilizando el macro SET_CUSTOM_FAKE_SEQ . Aquí hay un ejemplo:
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 );
} El falso llamará a sus funciones personalizadas en el orden especificado por el SET_CUSTOM_FAKE_SEQ macro. Cuando se alcanza la última falsa personalizada, el falso seguirá llamando a la última falsa personalizada en la secuencia. Esta macro funciona de manera muy similar a la macro SET_RETURN_SEQ .
Digamos que tienes dos funciones F1 y F2. F2 debe ser llamado a liberar algunos recursos asignados por F1, pero solo en los casos donde F1 devuelve cero. F1 podría ser pthread_mutex_trylock y F2 podría ser pthread_mutex_unlock. FFF guardará el historial de los valores devueltos para que esto se pueda verificar fácilmente, incluso cuando use una secuencia de falsificaciones personalizadas. Aquí hay un ejemplo 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]);
}
Acceda a los valores devueltos en el campo return_val_history .
Puede falsificar funciones variádicas utilizando Macros FAKE_VALUE_FUNC_VARARG y FAKE_VOID_FUNC_VARARG . Por ejemplo:
FAKE_VALUE_FUNC_VARARG(int, fprintf, FILE *, const char*, ...);
Para acceder a los parámetros variádicos desde una función falsa personalizada, declare un parámetro va_list . Por ejemplo, una falsificación personalizada para fprintf() podría llamar al verdad fprintf() como este:
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);
}
Al igual que los delegados de valor de retorno, también puede especificar secuencias para funciones variádicas utilizando SET_CUSTOM_FAKE_SEQ . Vea los archivos de prueba para ver ejemplos.
FFF tiene una capacidad limitada para habilitar la especificación de las convenciones visuales de llamadas C/C ++ de Microsoft, pero este soporte debe habilitarse al generar el archivo de encabezado de FFF fff.h
ruby fakegen.rb --with-calling-conventions > fff.h Al habilitar este soporte, todo el andamio de función falso de FFF requerirá la especificación de una convención de llamadas, por ejemplo, __cdecl para cada valor o falso nulo.
Aquí hay algunos ejemplos básicos: tenga en cuenta que la colocación de la convención de llamadas que se especifica es diferente dependiendo de si la falsa es una función vacía o de valor.
FAKE_VOID_FUNC ( __cdecl , voidfunc1 , int );
FAKE_VALUE_FUNC ( long , __cdecl , longfunc0 );El mecanismo básico que FFF le proporciona en este caso es el campo Custom_Fake descrito en el ejemplo del delegado de valor de retorno personalizado anterior.
Debe crear una función personalizada (por ejemplo, gettime_custom_fake) para producir la salida opcionalmente mediante el uso de una variable auxiliar (por ejemplo, gettime_custom_now) para recuperar esa salida. Luego, algo de creatividad para unirlo todo. La parte más importante (en mi humilde lugar) es mantener su caso de prueba legible y mantenible.
En caso de que su proyecto use un compilador C que admite funciones anidadas (por ejemplo, GCC), o cuando use lambdas C ++, incluso puede combinar todo esto en una sola función de prueba unitaria para que pueda supervisar fácilmente todos los detalles de la prueba.
#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 );
}Usar FFF para Stub Functions que tienen el parámetro de puntero de función puede causar problemas al intentar eliminarlas. Presentado aquí hay un ejemplo de cómo lidiar con esta situación.
Si necesita eliminar una función que tenga un parámetro de puntero de función, por ejemplo, algo así como:
/* timer.h */
typedef int timer_handle ;
extern int timer_start ( timer_handle handle , long delay , void ( * cb_function ) ( int arg ), int arg ); Luego, crear una falsa como a continuación fallará horriblemente al intentar compilar porque la macro FFF se expandirá internamente a una variable ilegal int (*)(int) arg2_val .
/* The fake, attempt one */
FAKE_VALUE_FUNC ( int ,
timer_start ,
timer_handle ,
long ,
void ( * ) ( int argument ),
int );La solución a este problema es crear un tipo de puente que solo necesita ser visible en el probador unitario. El falso usará ese tipo intermedio. De esta manera, el compilador no se quejará porque los tipos coinciden.
/* 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 );Aquí hay algunas ideas sobre cómo crear un caso de prueba con devoluciones de llamada.
/* 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 );
} FFF funciona como FAKE_VALUE_FUNC realizará la declaración y la definición de la función falsa y las estructuras de datos correspondientes. Esto no se puede colocar en un encabezado, ya que conducirá a múltiples definiciones de las funciones falsas.
La solución es separar la declaración y la definición de las falsificaciones, y colocar la declaración en un archivo de encabezado público, y la definición en un archivo fuente privado.
Aquí hay un ejemplo de cómo se podría hacer:
/* 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 , ...); Puede especificar los atributos de la función GCC para sus falsificaciones utilizando la Directiva FFF_GCC_FUNCTION_ATTRIBUTES .
Un atributo utilizado es el atributo débil que marca una función de tal manera que puede ser anulada por una variante que no sea de Weak en el tiempo de enlace. El uso de funciones débiles en combinación con FFF puede ayudar a simplificar su enfoque de prueba.
Por ejemplo:
Puede marcar todas las falsificaciones con el atributo débil como así:
#define FFF_GCC_FUNCTION_ATTRIBUTES __attribute__((weak))
#include "fff.h"
Vea el proyecto de ejemplo que demuestra el enfoque anterior: ./examples/weak_linking .
Mire en el directorio de ejemplos para ver ejemplos de longitud completa en C y C ++. También hay un conjunto de pruebas para el marco en el directorio de prueba.
Entonces, ¿cuál es el punto?
| Macro | Descripción | Ejemplo |
|---|---|---|
| False_void_func (fn [, arg_types*]); | Definir una función falsa llamada FN que regresa con n argumentos | False_void_func (display_output_message, const char*); |
| False_value_func (return_type, fn [, arg_types*]); | Definir una función falsa que devuelve un valor con type return_type tomando n argumentos | False_value_func (int, display_get_line_insert_index); |
| False_void_func_vararg (fn [, arg_types*], ...); | Definir una función variádica falsa que devuelve el tipo return_type tomando n argumentos y n argumentos variádicos | False_void_func_vararg (fn, const char*, ...) |
| False_value_func_vararg (return_type, fn [, arg_types*], ...); | Definir una función variádica falsa que devuelve un valor con el tipo return_type tomando n argumentos y n argumentos variádicos | False_value_func_vararg (int, fprintf, archivo*, const char*, ...) |
| Reset_fake (fn); | Restablecer el estado de la función falsa llamada FN | Reset_fake (display_init); |