FFF adalah kerangka mikro untuk membuat fungsi C palsu untuk tes. Karena hidup terlalu singkat untuk menghabiskan waktu untuk menulis fungsi palsu untuk pengujian.
Untuk menjalankan semua tes dan sampel aplikasi, cukup hubungi $ buildandtest . Script ini akan menelepon ke CMake dengan yang berikut:
cmake -B build -DFFF_GENERATE=ON -DFFF_UNIT_TESTING=ON
cmake --build build
ctest --test-dir build --output-on-failureKatakanlah Anda sedang menguji antarmuka pengguna tertanam dan Anda memiliki fungsi yang ingin Anda buat palsu untuk:
// UI.c
...
void DISPLAY_init ();
...Begini cara Anda mendefinisikan fungsi palsu untuk ini di suite tes Anda:
// test.c(pp)
#include "fff.h"
DEFINE_FFF_GLOBALS ;
FAKE_VOID_FUNC ( DISPLAY_init );Dan tes unit mungkin terlihat seperti ini:
TEST_F ( GreeterTests , init_initialises_display )
{
UI_init ();
ASSERT_EQ ( DISPLAY_init_fake . call_count , 1 );
} Jadi apa yang terjadi di sini? Hal pertama yang perlu diperhatikan adalah bahwa kerangka kerja hanya header, yang perlu Anda lakukan untuk menggunakannya adalah mengunduh fff.h dan memasukkannya ke dalam suite tes Anda.
Keajaiban ada di FAKE_VOID_FUNC . Ini memperluas makro yang mendefinisikan fungsi returning void yang tidak memiliki argumen. Ini juga mendefinisikan struct "function_name"_fake yang berisi semua informasi tentang palsu. Misalnya, DISPLAY_init_fake.call_count bertambah setiap kali fungsi palsu dipanggil.
Di bawah kap mesin menghasilkan struct yang terlihat seperti ini:
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 ;Oke, cukup dengan contoh mainan. Bagaimana dengan fungsi pemalsuan dengan argumen?
// UI.c
...
void DISPLAY_output ( char * message );
...Begini cara Anda mendefinisikan fungsi palsu untuk ini di suite tes Anda:
FAKE_VOID_FUNC ( DISPLAY_output , char * );Dan tes unit mungkin terlihat seperti ini:
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 );
} Tidak ada lagi sihir di sini, FAKE_VOID_FUNC berfungsi seperti pada contoh sebelumnya. Jumlah argumen yang diambil fungsi dihitung, dan argumen makro yang mengikuti nama fungsi mendefinisikan jenis argumen (penunjuk char dalam contoh ini).
Variabel dibuat untuk setiap argumen dalam bentuk "function_name"fake.argN_val
Ketika Anda ingin mendefinisikan fungsi palsu yang mengembalikan nilai, Anda harus menggunakan makro FAKE_VALUE_FUNC . Misalnya:
// UI.c
...
unsigned int DISPLAY_get_line_capacity ();
unsigned int DISPLAY_get_line_insert_index ();
...Begini cara Anda mendefinisikan fungsi palsu untuk ini di suite tes Anda:
FAKE_VALUE_FUNC ( unsigned int , DISPLAY_get_line_capacity );
FAKE_VALUE_FUNC ( unsigned int , DISPLAY_get_line_insert_index );Dan tes unit mungkin terlihat seperti ini:
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 );
}Tentu saja Anda dapat mencampur dan mencocokkan makro ini untuk menentukan fungsi nilai dengan argumen, misalnya untuk memalsukan:
double pow ( double base , double exponent );Anda akan menggunakan sintaks seperti ini:
FAKE_VALUE_FUNC ( double , pow , double , double );Tes yang baik adalah tes terisolasi, sehingga penting untuk mengatur ulang palsu untuk setiap tes unit. Semua palsu memiliki fungsi reset untuk mengatur ulang argumen dan penghitungan panggilan mereka. Ini adalah praktik yang baik adalah memanggil fungsi reset untuk semua pemalsuan dalam fungsi pengaturan suite tes Anda.
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 );
}Anda mungkin ingin mendefinisikan makro untuk melakukan ini:
/* 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 ();
}Katakanlah Anda ingin menguji bahwa fungsi memanggil fungsi, lalu functionB, lalu functionA lagi, bagaimana Anda melakukannya? Nah FFF mempertahankan riwayat panggilan sehingga mudah untuk menegaskan harapan ini.
Begini cara kerjanya:
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 );
} Mereka diatur ulang dengan menelepon FFF_RESET_HISTORY();
Kerangka kerja secara default akan menyimpan argumen untuk sepuluh panggilan terakhir yang dilakukan ke fungsi palsu.
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 ]);
}Ada dua cara untuk mengetahui apakah panggilan telah dijatuhkan. Yang pertama adalah memeriksa penghitung sejarah yang dijatuhkan:
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 );
}Yang lainnya adalah memeriksa apakah jumlah panggilan lebih besar dari ukuran riwayat:
ASSERT ( voidfunc2_fake . arg_history_len < voidfunc2_fake . call_count ); Sejarah argumen untuk fungsi palsu diatur ulang ketika fungsi RESET_FAKE disebut
Jika Anda ingin mengontrol berapa banyak panggilan yang harus ditangkap untuk riwayat argumen, Anda dapat mengesampingkan default dengan mendefinisikannya sebelum menyertakan fff.h seperti ini:
// 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" Seringkali dalam pengujian kami ingin menguji perilaku urutan peristiwa panggilan fungsi. Salah satu cara untuk melakukan ini dengan FFF adalah dengan menentukan urutan nilai pengembalian dengan untuk fungsi palsu. Mungkin lebih mudah dijelaskan dengan contoh:
// 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 ());
} Dengan menentukan urutan nilai pengembalian menggunakan makro SET_RETURN_SEQ , palsu akan mengembalikan nilai yang diberikan dalam array parameter secara berurutan. Ketika akhir urutan tercapai, palsu akan terus mengembalikan nilai terakhir dalam urutan tanpa batas.
Anda dapat menentukan fungsi Anda sendiri untuk memberikan nilai pengembalian untuk palsu. Ini dilakukan dengan mengatur anggota Fake custom_fake . Inilah contohnya:
#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 );
} Katakanlah Anda memiliki fungsi dengan parameter keluar, dan Anda ingin memiliki perilaku yang berbeda pada tiga panggilan pertama, misalnya: Atur nilai 'x' ke parameter keluar pada panggilan pertama, nilai 'y' ke parameter keluar pada panggilan kedua, dan nilai 'z' ke parameter keluar pada panggilan ketiga. Anda dapat menentukan urutan fungsi khusus ke fungsi non-variadik menggunakan makro SET_CUSTOM_FAKE_SEQ . Inilah contohnya:
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 );
} Palsu akan menghubungi fungsi khusus Anda dalam urutan yang ditentukan oleh makro SET_CUSTOM_FAKE_SEQ . Ketika Fake Kustom Terakhir dicapai, palsu akan terus memanggil palsu terakhir dalam urutan. Makro ini berfungsi seperti makro SET_RETURN_SEQ .
Katakanlah Anda memiliki dua fungsi F1 dan F2. F2 harus dipanggil untuk melepaskan beberapa sumber daya yang dialokasikan oleh F1, tetapi hanya dalam kasus di mana F1 mengembalikan nol. F1 bisa pthread_mutex_trylock dan F2 bisa pthread_mutex_unlock. FFF akan menyimpan riwayat nilai yang dikembalikan sehingga ini dapat dengan mudah diperiksa, bahkan ketika Anda menggunakan urutan pemalsuan khusus. Inilah contoh sederhana:
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]);
}
Anda mengakses nilai yang dikembalikan di bidang return_val_history .
Anda dapat memalsukan fungsi variadik menggunakan makro FAKE_VALUE_FUNC_VARARG dan FAKE_VOID_FUNC_VARARG . Misalnya:
FAKE_VALUE_FUNC_VARARG(int, fprintf, FILE *, const char*, ...);
Untuk mengakses parameter variadik dari fungsi palsu khusus, nyatakan parameter va_list . Misalnya, palsu khusus untuk fprintf() dapat memanggil fprintf() seperti ini:
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);
}
Sama seperti delegasi nilai pengembalian, Anda juga dapat menentukan urutan untuk fungsi variadik menggunakan SET_CUSTOM_FAKE_SEQ . Lihat file uji untuk contoh.
FFF memiliki kemampuan terbatas untuk memungkinkan spesifikasi konvensi panggilan Visual C/C ++ visual Microsoft, tetapi dukungan ini harus diaktifkan saat menghasilkan file header FFF fff.h
ruby fakegen.rb --with-calling-conventions > fff.h Dengan mengaktifkan dukungan ini, semua perancah fungsi palsu FFF akan mengharuskan spesifikasi konvensi panggilan, misalnya __cdecl untuk setiap nilai atau batal palsu.
Berikut adalah beberapa contoh dasar: Perhatikan bahwa penempatan konvensi panggilan yang ditentukan berbeda tergantung pada apakah palsu adalah fungsi void atau nilai.
FAKE_VOID_FUNC ( __cdecl , voidfunc1 , int );
FAKE_VALUE_FUNC ( long , __cdecl , longfunc0 );Mekanisme dasar yang diberikan FFF kepada Anda dalam hal ini adalah bidang Custom_Fake yang dijelaskan dalam contoh Delegasi Nilai Pengembalian Kustom di atas.
Anda perlu membuat fungsi khusus (misalnya gettime_custom_fake) untuk menghasilkan output secara opsional dengan menggunakan variabel helper (misalnya gettime_custom_now) untuk mengambil output dari. Kemudian beberapa kreativitas untuk mengikat semuanya. Bagian terpenting (IMHO) adalah menjaga kasus uji Anda dapat dibaca dan dapat dipelihara.
Jika proyek Anda menggunakan kompiler C yang mendukung fungsi bersarang (misalnya GCC), atau saat menggunakan c ++ lambdas, Anda bahkan dapat menggabungkan semua ini dalam satu fungsi uji unit tunggal sehingga Anda dapat dengan mudah mengawasi semua detail tes.
#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 );
}Menggunakan FFF untuk fungsi rintisan yang memiliki parameter pointer fungsi dapat menyebabkan masalah ketika mencoba untuk mematikannya. Disajikan di sini adalah contoh bagaimana menangani situasi ini.
Jika Anda perlu mematikan fungsi yang memiliki parameter pointer fungsi, misalnya sesuatu seperti:
/* timer.h */
typedef int timer_handle ;
extern int timer_start ( timer_handle handle , long delay , void ( * cb_function ) ( int arg ), int arg ); Kemudian membuat palsu seperti di bawah ini akan sangat gagal ketika mencoba untuk dikompilasi karena makro FFF akan secara internal berkembang menjadi variabel ilegal int (*)(int) arg2_val .
/* The fake, attempt one */
FAKE_VALUE_FUNC ( int ,
timer_start ,
timer_handle ,
long ,
void ( * ) ( int argument ),
int );Solusi untuk masalah ini adalah membuat tipe bridging yang hanya perlu terlihat di unit tester. Palsu akan menggunakan tipe perantara itu. Dengan cara ini kompiler tidak akan mengeluh karena jenisnya cocok.
/* 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 );Berikut adalah beberapa ide cara membuat test case dengan callbacks.
/* 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 );
} Fungsi FFF seperti FAKE_VALUE_FUNC akan melakukan deklarasi dan definisi fungsi palsu dan struct data yang sesuai. Ini tidak dapat ditempatkan di header, karena akan menyebabkan beberapa definisi fungsi palsu.
Solusinya adalah memisahkan deklarasi dan definisi palsu, dan menempatkan deklarasi ke dalam file header publik, dan definisi ke dalam file sumber pribadi.
Berikut adalah contoh bagaimana hal itu bisa dilakukan:
/* 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 , ...); Anda dapat menentukan atribut fungsi GCC untuk pemalsuan Anda menggunakan arahan FFF_GCC_FUNCTION_ATTRIBUTES .
Salah satu atribut yang berguna adalah atribut lemah yang menandai fungsi sedemikian rupa sehingga dapat ditimpa oleh varian non-lemah pada waktu tautan. Menggunakan fungsi yang lemah dalam kombinasi dengan FFF dapat membantu menyederhanakan pendekatan pengujian Anda.
Misalnya:
Anda dapat menandai semua pemalsuan dengan atribut lemah seperti itu:
#define FFF_GCC_FUNCTION_ATTRIBUTES __attribute__((weak))
#include "fff.h"
Lihat contoh proyek yang menunjukkan pendekatan di atas: ./examples/weak_linking .
Lihat di bawah Direktori Contoh untuk contoh panjang penuh di kedua C dan C ++. Ada juga suite tes untuk kerangka kerja di bawah direktori tes.
Jadi apa gunanya?
| Makro | Keterangan | Contoh |
|---|---|---|
| Fake_void_func (fn [, arg_types*]); | Tentukan fungsi palsu bernama FN Returning Void dengan N Argumen | Fake_void_func (display_output_message, const char*); |
| Fake_value_func (return_type, fn [, arg_types*]); | Tentukan fungsi palsu mengembalikan nilai dengan tipe return_type mengambil n argumen | Fake_value_func (int, display_get_line_insert_index); |
| Fake_void_func_vararg (fn [, arg_types*], ...); | Tentukan fungsi variadik palsu yang mengembalikan batal dengan tipe return_type mengambil n argumen dan n variadik argumen | Fake_void_func_vararg (fn, const char*, ...) |
| Fale_value_func_vararg (return_type, fn [, arg_types*], ...); | Tentukan fungsi variadik palsu yang mengembalikan nilai dengan tipe return_type mengambil n argumen dan n argumen variadik | Fake_value_func_vararg (int, fprintf, file*, const char*, ...) |
| Reset_fake (fn); | Setel ulang keadaan fungsi palsu yang disebut fn | Reset_fake (display_init); |