O FFF é um micro-quadro para criar funções C falsas para testes. Porque a vida é muito curta para gastar tempo a escrever funções falsas para testes.
Para executar todos os testes e aplicativos de amostra, basta chamar $ buildandtest . Este script será chamado para o cmake com o seguinte:
cmake -B build -DFFF_GENERATE=ON -DFFF_UNIT_TESTING=ON
cmake --build build
ctest --test-dir build --output-on-failureDigamos que você esteja testando uma interface de usuário incorporada e tem uma função para a qual deseja criar uma farsa:
// UI.c
...
void DISPLAY_init ();
...Veja como você definiria uma função falsa para isso no seu conjunto de testes:
// test.c(pp)
#include "fff.h"
DEFINE_FFF_GLOBALS ;
FAKE_VOID_FUNC ( DISPLAY_init );E o teste de unidade pode parecer algo assim:
TEST_F ( GreeterTests , init_initialises_display )
{
UI_init ();
ASSERT_EQ ( DISPLAY_init_fake . call_count , 1 );
} Então, o que aconteceu aqui? A primeira coisa a observar é que a estrutura é apenas o cabeçalho, tudo o que você precisa fazer para usá -lo é baixar fff.h e incluí -lo no seu conjunto de testes.
A mágica está no FAKE_VOID_FUNC . Isso expande uma macro que define uma função que retorna void que possui zero argumentos. Ele também define um struct "function_name"_fake , que contém todas as informações sobre o falso. Por exemplo, DISPLAY_init_fake.call_count é incrementado toda vez que a função falsificada é chamada.
Sob o capô, ele gera uma estrutura que se parece com a seguinte:
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, chega de exemplos de brinquedos. Que tal fingir funções com argumentos?
// UI.c
...
void DISPLAY_output ( char * message );
...Veja como você definiria uma função falsa para isso no seu conjunto de testes:
FAKE_VOID_FUNC ( DISPLAY_output , char * );E o teste de unidade pode parecer algo assim:
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 );
} Não há mais mágica aqui, o FAKE_VOID_FUNC funciona como no exemplo anterior. O número de argumentos que a função assume é calculado e os argumentos macro seguindo o nome da função define o tipo de argumento (um ponteiro de char neste exemplo).
Uma variável é criada para todos os argumentos no formulário "function_name"fake.argN_val
Quando você deseja definir uma função falsa que retorne um valor, você deve usar a macro FAKE_VALUE_FUNC . Por exemplo:
// UI.c
...
unsigned int DISPLAY_get_line_capacity ();
unsigned int DISPLAY_get_line_insert_index ();
...Veja como você definiria funções falsas para elas em sua suíte de teste:
FAKE_VALUE_FUNC ( unsigned int , DISPLAY_get_line_capacity );
FAKE_VALUE_FUNC ( unsigned int , DISPLAY_get_line_insert_index );E o teste de unidade pode parecer algo assim:
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 );
}É claro que você pode misturar e combinar essas macros para definir uma função de valor com argumentos, por exemplo, para fingir:
double pow ( double base , double exponent );Você usaria uma sintaxe como esta:
FAKE_VALUE_FUNC ( double , pow , double , double );Bons testes são testes isolados, por isso é importante redefinir as falsificações para cada teste de unidade. Todas as falsificações têm uma função de redefinição para redefinir seus argumentos e contagens de chamadas. É uma boa prática chamar a função de redefinição de todas as falsificações na função de configuração do seu conjunto de testes.
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 );
}Você pode querer definir uma macro para fazer isso:
/* 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 você queira testar que uma função chama functionA, depois functionb e depois functiona novamente, como você faria isso? Bem, o FFF mantém um histórico de chamadas para que seja fácil afirmar essas expectativas.
Aqui está 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 );
} Eles são redefinidos ligando para FFF_RESET_HISTORY();
A estrutura, por padrão, armazenará os argumentos para as últimas dez chamadas feitas para uma função 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 ]);
}Há duas maneiras de descobrir se as chamadas foram descartadas. O primeiro é verificar o balcão de histórias descartadas:
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 );
}O outro é verificar se a contagem de chamadas é maior que o tamanho do histórico:
ASSERT ( voidfunc2_fake . arg_history_len < voidfunc2_fake . call_count ); As histórias de argumento para uma função falsa são redefinidas quando a função RESET_FAKE é chamada
Se você deseja controlar quantas chamadas capturarem o histórico de argumentos, você pode substituir o padrão, definindo -o antes de incluir o fff.h como este:
// 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" Muitas vezes, nos testes, gostaríamos de testar o comportamento da sequência de eventos de chamada de função. Uma maneira de fazer isso com o FFF é especificar uma sequência de valores de retorno para a função falsa. Provavelmente é mais fácil de descrever com um exemplo:
// 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 ());
} Ao especificar uma sequência de valor de retorno usando a macro SET_RETURN_SEQ , o falso retornará os valores fornecidos na matriz de parâmetros na sequência. Quando o final da sequência for atingido, o falso continuará retornando o último valor na sequência indefinidamente.
Você pode especificar sua própria função para fornecer o valor de retorno para o falso. Isso é feito definindo o membro custom_fake do falso. Aqui está um exemplo:
#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 você tenha uma função com um parâmetro out e deseja que ele tenha um comportamento diferente nas três primeiras chamadas, por exemplo: defina o valor 'x' no parâmetro out na primeira chamada, o valor 'y' para o parâmetro out na segunda chamada e o valor 'z' para o parâmetro out na terceira chamada. Você pode especificar uma sequência de funções personalizadas para uma função não-váriada usando a macro SET_CUSTOM_FAKE_SEQ . Aqui está um exemplo:
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 );
} O falso chamará suas funções personalizadas no pedido especificado pela macro SET_CUSTOM_FAKE_SEQ . Quando a última falsa personalizada for alcançada, o falso continuará chamando a última falsa personalizada na sequência. Essa macro funciona como a macro SET_RETURN_SEQ .
Digamos que você tenha duas funções F1 e F2. F2 deve ser chamado para liberar algum recurso alocado pela F1, mas apenas nos casos em que a F1 retorna zero. F1 pode ser pthread_mutex_trylock e f2 pode ser pthread_mutex_unlock. O FFF salvará o histórico dos valores retornados para que isso possa ser facilmente verificado, mesmo quando você usa uma sequência de falsificações personalizadas. Aqui está um exemplo simples:
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]);
}
Você acessa os valores retornados no campo return_val_history .
Você pode falsificar funções variáticas usando as macros FAKE_VALUE_FUNC_VARARG e FAKE_VOID_FUNC_VARARG . Por exemplo:
FAKE_VALUE_FUNC_VARARG(int, fprintf, FILE *, const char*, ...);
Para acessar os parâmetros variádicos de uma função falsa personalizada, declare um parâmetro va_list . Por exemplo, uma falsa personalizada para fprintf() poderia chamar o verdadeiro 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);
}
Assim como os delegados do valor de retorno, você também pode especificar sequências para funções variádicas usando SET_CUSTOM_FAKE_SEQ . Consulte os arquivos de teste para obter exemplos.
O FFF possui uma capacidade limitada para permitir a especificação das convenções de chamada visual de C/C ++ da Microsoft, mas esse suporte deve ser ativado ao gerar o arquivo de cabeçalho da FFF fff.h
ruby fakegen.rb --with-calling-conventions > fff.h Ao ativar esse suporte, todos os andaimes de função falsa da FFF exigirão a especificação de uma convenção de chamada, por exemplo, __cdecl para cada valor ou falso falso.
Aqui estão alguns exemplos básicos: observe que a colocação da convenção de chamada que está sendo especificada é diferente, dependendo de o falso ser uma função de vazio ou valor.
FAKE_VOID_FUNC ( __cdecl , voidfunc1 , int );
FAKE_VALUE_FUNC ( long , __cdecl , longfunc0 );O mecanismo básico que o FFF fornece neste caso é o campo Custom_Fake descrito no exemplo de delegado de valor de retorno personalizado acima.
Você precisa criar uma função personalizada (por exemplo, gettime_custom_fake) para produzir a saída opcionalmente pelo uso de uma variável auxiliar (por exemplo, gettime_custom_now) para recuperar essa saída. Então, alguma criatividade para amarrar tudo. A parte mais importante (IMHO) é manter o seu caso de teste legível e sustentável.
Caso seu projeto use um compilador C que suporta funções aninhadas (por exemplo, GCC) ou, ao usar o C ++ Lambdas, você pode até combinar tudo isso em uma única função de teste de unidade para que você possa supervisionar facilmente todos os detalhes do teste.
#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 );
}O uso de funções FFF para Stub que possuem parâmetro de ponteiro de função pode causar problemas ao tentar arrancá -los. Apresentado aqui é um exemplo de como lidar com essa situação.
Se você precisar extrair uma função que tenha um parâmetro de ponteiro de função, por exemplo, algo como:
/* timer.h */
typedef int timer_handle ;
extern int timer_start ( timer_handle handle , long delay , void ( * cb_function ) ( int arg ), int arg ); Em seguida, criar uma falsa como abaixo falhará horrivelmente ao tentar compilar, porque a macro FFF se expandirá internamente para uma variável ilegal int (*)(int) arg2_val .
/* The fake, attempt one */
FAKE_VALUE_FUNC ( int ,
timer_start ,
timer_handle ,
long ,
void ( * ) ( int argument ),
int );A solução para esse problema é criar um tipo de ponte que precisa ser visível apenas no testador da unidade. O falso usará esse tipo intermediário. Dessa forma, o compilador não reclamará porque os tipos correspondem.
/* 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 );Aqui estão algumas idéias de como criar um caso de teste com retornos de chamada.
/* 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 );
} As funções do FFF como FAKE_VALUE_FUNC executarão a declaração e a definição da função falsa e as estruturas de dados correspondentes. Isso não pode ser colocado em um cabeçalho, pois levará a várias definições das funções falsas.
A solução é separar a declaração e a definição das falsificações e colocar a declaração em um arquivo de cabeçalho público e a definição em um arquivo de fonte privada.
Aqui está um exemplo de como isso pode ser feito:
/* 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 , ...); Você pode especificar atributos da função GCC para suas falsificações usando a diretiva FFF_GCC_FUNCTION_ATTRIBUTES .
Um atributo usful é o atributo fraco que marca uma função de modo que ele possa ser substituído por uma variante não weak no horário do link. Usar funções fracas em combinação com o FFF pode ajudar a simplificar sua abordagem de teste.
Por exemplo:
Você pode marcar todas as falsificações com o atributo fraco como assim:
#define FFF_GCC_FUNCTION_ATTRIBUTES __attribute__((weak))
#include "fff.h"
Consulte o projeto de exemplo que demonstra a abordagem acima: ./examples/weak_linking .
Olhe no diretório Exemplos para exemplos de comprimento total em C e C ++. Há também um conjunto de testes para a estrutura no diretório de teste.
Então, qual é o ponto?
| Macro | Descrição | Exemplo |
|---|---|---|
| Falso_void_func (fn [, arg_types*]); | Defina uma função falsa chamada fn retornando vazio com n argumentos | Falso_void_func (display_output_message, const char*); |
| Falso_value_func (return_type, fn [, arg_types*]); | Defina uma função falsa retornando um valor com tipo Return_type recebendo n argumentos | Falso_value_func (int, display_get_line_insert_index); |
| Falso_void_func_vararg (fn [, arg_types*], ...); | Defina uma função variada falsa retornando vazio com tipo Return_type recebendo n argumentos e n argumentos variáticos | Falso_void_func_vararg (fn, const char*, ...) |
| Falso_value_func_vararg (return_type, fn [, arg_types*], ...); | Defina uma função variada falsa retornando um valor com tipo Return_type recebendo n argumentos e n argumentos variáticos | Falso_value_func_vararg (int, fprintf, arquivo*, const char*, ...) |
| Reset_fake (fn); | Redefina o estado da função falsa chamada fn | Reset_fake (display_init); |