FFF是用于为测试创建假C函数的微型框架。因为生活太短了,无法花时间手工编写的假函数进行测试。
要运行所有测试和示例应用程序,只需致电$ buildandtest 。此脚本将以以下内容呼吁Cmake:
cmake -B build -DFFF_GENERATE=ON -DFFF_UNIT_TESTING=ON
cmake --build build
ctest --test-dir build --output-on-failure假设您正在测试嵌入式用户界面,并且您的功能要创建一个伪造的功能:
// UI.c
...
void DISPLAY_init ();
...这是您在测试套件中为此定义假函数的方法:
// test.c(pp)
#include "fff.h"
DEFINE_FFF_GLOBALS ;
FAKE_VOID_FUNC ( DISPLAY_init );单位测试可能看起来像这样:
TEST_F ( GreeterTests , init_initialises_display )
{
UI_init ();
ASSERT_EQ ( DISPLAY_init_fake . call_count , 1 );
}那么这里发生了什么?要注意的第一件事是,该框架仅是标题,您要使用的只是下载fff.h并将其包含在您的测试套件中。
魔术在FAKE_VOID_FUNC中。这扩展了一个宏,该宏定义一个返回的函数,该函数的void为零参数。它还定义了一个结构"function_name"_fake ,其中包含有关假货的所有信息。例如,每次调用伪造函数时, DISPLAY_init_fake.call_count都会增加。
在引擎盖下,它产生了一个看起来像这样的结构:
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 ;好的,有足够的玩具示例。伪造有争议的功能呢?
// UI.c
...
void DISPLAY_output ( char * message );
...这是您在测试套件中为此定义假函数的方法:
FAKE_VOID_FUNC ( DISPLAY_output , char * );单位测试可能看起来像这样:
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 );
}这里不再有魔术, FAKE_VOID_FUNC工作如上一个示例。该函数采用的参数数量是计算的,并且函数名称之后的宏参数定义了参数类型(此示例中的char指针)。
为"function_name"fake.argN_val中的每个参数创建一个变量
当您要定义返回值的假函数时,应使用FAKE_VALUE_FUNC宏。例如:
// UI.c
...
unsigned int DISPLAY_get_line_capacity ();
unsigned int DISPLAY_get_line_insert_index ();
...这是您在测试套件中为这些定义这些伪造功能的方法:
FAKE_VALUE_FUNC ( unsigned int , DISPLAY_get_line_capacity );
FAKE_VALUE_FUNC ( unsigned int , DISPLAY_get_line_insert_index );单位测试可能看起来像这样:
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 );
}当然,您可以混合并匹配这些宏以用参数定义值函数,例如伪造:
double pow ( double base , double exponent );您将使用这样的语法:
FAKE_VALUE_FUNC ( double , pow , double , double );良好的测试是孤立的测试,因此为每个单位测试重置假货很重要。所有的假货都具有重置函数来重置他们的论点和呼叫计数。好的做法是将测试套件设置功能中所有假货的重置功能调用。
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 );
}您可能需要定义一个宏来执行此操作:
/* 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 ();
}假设您要测试一个函数调用功能A,然后函数B,然后再次功能A,您将如何做? FFF保持了呼叫历史记录,因此可以轻松地确定这些期望。
这是其工作原理:
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 );
}通过调用FFF_RESET_HISTORY();
默认情况下,该框架将存储对伪造功能的最后十个呼叫的参数。
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 ]);
}有两种方法可以找出是否已拨打电话。首先是检查掉落的历史计数器:
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 );
}另一个是检查呼叫计数是否大于历史记录大小:
ASSERT ( voidfunc2_fake . arg_history_len < voidfunc2_fake . call_count );当RESET_FAKE函数称为伪造功能的参数历史记录是重置的
如果您希望控制有多少次捕获参数历史记录的呼叫,则可以通过定义默认值来覆盖默认值,然后再包含fff.h ,这样
// 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" 通常,在测试中,我们想测试函数呼叫事件序列的行为。使用FFF执行此操作的一种方法是指定一个伪造函数的返回值序列。用一个示例描述可能更容易:
// 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 ());
}通过使用SET_RETURN_SEQ宏指定返回值序列,该假将返回序列参数数组中给出的值。到达序列的末尾时,假货将继续无限期地返回序列中的最后一个值。
您可以指定自己的功能,以提供伪造的返回值。这是通过设置伪造的custom_fake成员来完成的。这是一个例子:
#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 );
}假设您的功能具有带有参数的函数,并且您希望它在前三个呼叫上具有不同的行为,例如:将值'x'设置为第一个调用上的out参数,将值'y',在第二个调用中的out参数,而值'z'在第三个呼叫上将值'z'设置为OUT参数。您可以使用SET_CUSTOM_FAKE_SEQ宏来指定自定义函数序列。这是一个例子:
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 );
}假货将以SET_CUSTOM_FAKE_SEQ宏指定的顺序调用您的自定义功能。当达到最后一个自定义假货时,假货将继续在序列中调用最后一个自定义假的假。该宏的工作原理与SET_RETURN_SEQ宏观相似。
假设您有两个功能F1和F2。必须调用F2发布F1分配的一些资源,但仅在F1返回零的情况下。 F1可以是pthread_mutex_trylock,而F2可以是pthread_mutex_unlock。 FFF将保存返回值的历史记录,因此即使您使用一系列自定义假货,也可以轻松地检查。这是一个简单的例子:
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]);
}
您在return_val_history字段中访问返回的值。
您可以使用宏FAKE_VALUE_FUNC_VARARG和FAKE_VOID_FUNC_VARARG伪造variadic函数。例如:
FAKE_VALUE_FUNC_VARARG(int, fprintf, FILE *, const char*, ...);
为了从自定义伪造功能访问variadic参数,请声明va_list参数。例如, fprintf()的自定义伪造可以像这样调用真实的fprintf() :
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);
}
就像返回值委托一样,您也可以使用SET_CUSTOM_FAKE_SEQ为variadic函数指定序列。请参阅测试文件以获取示例。
FFF的功能有限,可以启用Microsoft的Visual C/C ++调用约定的规范,但是在生成FFF的Header File fff.h时必须启用此支持。
ruby fakegen.rb --with-calling-conventions > fff.h通过启用此支持,FFF的所有假函数脚手架都需要规范呼叫__cdecl ,例如每个值或虚拟假货。
以下是一些基本示例:请注意,指定的调用约定的放置是不同的,具体取决于假货是空隙还是值函数。
FAKE_VOID_FUNC ( __cdecl , voidfunc1 , int );
FAKE_VALUE_FUNC ( long , __cdecl , longfunc0 );在这种情况下,FFF为您提供的基本机制是上面的自定义返回值委托示例中描述的custom_fake字段。
您需要创建一个自定义函数(例如getTime_custom_fake),通过使用助手变量(例如gettime_custom_now)可选地产生输出。然后有一些创造力将它们捆绑在一起。最重要的部分(IMHO)是使您的测试案例可读且可维护。
如果您的项目使用支持嵌套功能(例如GCC)的C编译器,或者使用C ++ lambdas时,您甚至可以将所有这些组合在单个单元测试功能中,以便您可以轻松地监督测试的所有详细信息。
#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 );
}使用具有函数指针参数的FFF到存根函数,可能会在尝试存根时引起问题。这里介绍的是如何处理这种情况的一个例子。
如果您需要存根具有功能指针参数的函数,例如:
/* timer.h */
typedef int timer_handle ;
extern int timer_start ( timer_handle handle , long delay , void ( * cb_function ) ( int arg ), int arg );然后,在尝试编译时创建一个像下面的假货将非常失败,因为FFF宏将在内部扩展到非法变量int (*)(int) arg2_val 。
/* The fake, attempt one */
FAKE_VALUE_FUNC ( int ,
timer_start ,
timer_handle ,
long ,
void ( * ) ( int argument ),
int );解决此问题的解决方案是创建仅在单元测试仪中可见的桥接类型。假货将使用该中间类型。这样,编译器将不会抱怨,因为类型匹配。
/* 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 );以下是一些想法如何用回调创建测试案例。
/* 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 );
}诸如FAKE_VALUE_FUNC之类的FFF函数将同时执行伪造函数和相应数据结构的声明和定义。这不能放在标题中,因为它将导致假函数的多个定义。
解决方案是将伪造的声明和定义分开,并将声明放入公共标题文件中,并将定义放入私人源文件中。
这是如何完成的示例:
/* 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 , ...);您可以使用FFF_GCC_FUNCTION_ATTRIBUTES指令为假货指定GCC功能属性。
一个利用的属性是标记一个函数的弱属性,以便在链接时间的非运动变体可以覆盖它。将弱功能与FFF结合使用可以帮助简化您的测试方法。
例如:
您可以将所有假货标记为弱属性这样的假货:
#define FFF_GCC_FUNCTION_ATTRIBUTES __attribute__((weak))
#include "fff.h"
请参阅演示上述方法的示例项目: ./ examples/weak_linking。
在示例目录下查找C和C ++中的全长示例。在测试目录下还有一个用于框架的测试套件。
那重点是什么?
| 宏 | 描述 | 例子 |
|---|---|---|
| fack_void_func(fn [,arg_types*]); | 定义一个名为fn的伪造函数,返回void,并用n个参数 | face_void_func(display_output_message,const char*); |
| face_value_func(return_type,fn [,arg_types*]); | 定义一个假函数,返回一个type return_type进行n参数 | face_value_func(int,display_get_line_insert_index); |
| fack_void_func_vararg(fn [,arg_types*],...); | 定义一个伪造的变异函数,使用type return_type返回void,以n参数和n个变量参数 | face_void_func_vararg(fn,const char*,...) |
| face_value_func_vararg(return_type,fn [,arg_types*],...); | 定义一个伪造的变异函数,返回一个type return_type进行n参数和n个variadic参数 | face_value_func_vararg(int,fprintf,file*,const char*,...) |
| reset_fake(fn); | 重置称为FN的假函数状态 | reset_fake(display_init); |