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 );الاختبارات الجيدة هي اختبارات معزولة ، لذلك من المهم إعادة تعيين Fakes لكل اختبار وحدة. جميع المزيفات لديها وظيفة إعادة تعيين لإعادة تعيين وسيطاتهم ومكالماتهم. من الممارسات الجيدة استدعاء وظيفة إعادة التعيين لجميع مزيفة في وظيفة الإعداد لمجموعة الاختبار الخاصة بك.
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 ();
}لنفترض أنك تريد اختبار وظيفة استدعاء الوظيفة ، ثم الوظيفة ، ثم الوظيفة مرة أخرى ، كيف يمكنك أن تفعل ذلك؟ حسنًا ، يحافظ 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" على المعلمة الخارجية في المكالمة الثالثة. يمكنك تحديد سلسلة من الوظائف المخصصة لدالة غير متغيرة باستخدام Macro 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 );
} سيقوم المزيف بالاتصال بوظائفك المخصصة بالترتيب المحدد بواسطة Macro SET_CUSTOM_FAKE_SEQ . عندما يتم الوصول إلى آخر مزيفة مخصصة ، سيستمر المزيف في استدعاء آخر مخصص في التسلسل. يعمل هذا الماكرو إلى حد كبير مثل Macro 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 . على سبيل المثال:
FAKE_VALUE_FUNC_VARARG(int, fprintf, FILE *, const char*, ...);
من أجل الوصول إلى المعلمات المتنوعة من وظيفة مزيفة مخصصة ، أعلن معلمة va_list . على سبيل المثال ، يمكن أن تسمي FPRINTF 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);
}
تمامًا مثل مندوبي قيمة الإرجاع ، يمكنك أيضًا تحديد تسلسلات لوظائف variadic باستخدام SET_CUSTOM_FAKE_SEQ . انظر ملفات الاختبار للحصول على أمثلة.
يتمتع FFF بإمكانية محدودة لتمكين مواصفات اتفاقيات استدعاء Microsoft المرئية C/C ++ ، ولكن يجب تمكين هذا الدعم عند إنشاء ملف رأس FFF 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) هو الحفاظ على قابلية اختبار حالة الاختبار الخاصة بك.
في حال كان مشروعك يستخدم برنامج التحويل البرمجي C يدعم الوظائف المتداخلة (مثل GCC) ، أو عند استخدام 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 );
} ستؤدي وظائف FFF مثل FAKE_VALUE_FUNC على كل من الإعلان وتعريف الوظيفة المزيفة وهياكل البيانات المقابلة. لا يمكن وضع هذا في رأس ، لأنه سيؤدي إلى تعريفات متعددة للوظائف المزيفة.
يتمثل الحل في فصل الإعلان وتعريف المزيفين ، ووضع الإعلان في ملف رأس عام ، والتعريف في ملف مصدر خاص.
فيما يلي مثال على كيفية القيام بذلك:
/* 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 , ...); يمكنك تحديد سمات وظيفة GCC لتوجيه FFF_GCC_FUNCTION_ATTRIBUTES .
تتمثل إحدى السمات الفاشلة في السمة الضعيفة التي تمثل وظيفة بحيث يمكن تجاوزها بواسطة متغير غير متكافئ في وقت الارتباط. يمكن أن يساعد استخدام الوظائف الضعيفة مع FFF في تبسيط نهج الاختبار الخاص بك.
على سبيل المثال:
يمكنك وضع علامة على كل مزيفة مع السمة الضعيفة مثل:
#define FFF_GCC_FUNCTION_ATTRIBUTES __attribute__((weak))
#include "fff.h"
راجع مشروع المثال الذي يوضح النهج أعلاه: ./examples/weak_linking .
انظر تحت دليل الأمثلة للحصول على أمثلة كاملة الطول في كل من C و C ++. هناك أيضًا مجموعة اختبار للإطار تحت دليل الاختبار.
إذن ما هي النقطة؟
| الماكرو | وصف | مثال |
|---|---|---|
| fake_void_func (fn [، arg_types*]) ؛ | حدد وظيفة مزيفة تدعى FN Returning Void مع وسيطات n | fake_void_func (display_output_message ، const char*) ؛ |
| fake_value_func (return_type ، fn [، arg_types*]) ؛ | تحديد وظيفة مزيفة لإرجاع قيمة مع type return_type أخذ وسيطات n | fake_value_func (int ، display_get_line_insert_index) ؛ |
| fake_void_func_vararg (fn [، arg_types*] ، ...) ؛ | تحديد وظيفة متنوعة وهمية إرجاع الفراغ مع type return_type أخذ وسيطات n والوسائط المتنوعة n | fake_void_func_vararg (fn ، const char*، ...) |
| fake_value_func_vararg (return_type ، fn [، arg_types*] ، ...) ؛ | حدد وظيفة متغيرات مزيفة لإرجاع قيمة مع Type Return_type أخذ وسيطات N و N Nowdic Minters | fake_value_func_vararg (int ، fprintf ، ملف*، const char*، ...) |
| refet_fake (fn) ؛ | إعادة ضبط حالة الوظيفة المزيفة تسمى fn | Reset_fake (display_init) ؛ |