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); |