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 ();
}함수가 functiona를 호출 한 다음 functionb, 다음에 다시 기능하는 것을 테스트하고 싶다고 가정 해 봅시다. 어떻게 그렇게 하시겠습니까? Well 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();
프레임 워크는 기본적으로 가짜 함수에 대한 마지막 10 번의 호출에 대한 인수를 저장합니다.
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 );
} Out 매개 변수가있는 함수가 있고 첫 번째 3 호출에서 다른 동작을 원한다고 가정하십시오 (예 : 첫 번째 호출의 값 'x'를, 두 번째 호출의 값 'y'값을 Out 매개 변수로 설정하고, 값 '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의 두 가지 기능이 있다고 가정하십시오. F1은 F1에 의해 할당 된 일부 리소스를 해제하려면 F2를 호출해야하지만 F1은 0을 반환하는 경우에만 호출해야합니다. 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 필드에서 반환 된 값에 액세스합니다.
macros 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의 헤더 파일 fff.h 를 생성 할 때는이 지원이 활성화되어야합니다.
ruby fakegen.rb --with-calling-conventions > fff.h 이 지원을 가능하게함으로써 FFF의 모든 가짜 함수 스캐 폴딩은 각 값 또는 무효 가짜에 대한 __cdecl (예 : __cdecl 의 사양이 필요합니다.
몇 가지 기본 예는 다음과 같습니다. 가짜가 무효인지 값 기능인지 여부에 따라 지정된 통화 규칙의 배치는 다릅니다.
FAKE_VOID_FUNC ( __cdecl , voidfunc1 , int );
FAKE_VALUE_FUNC ( long , __cdecl , longfunc0 );이 경우 FFF가 제공하는 기본 메커니즘은 위의 사용자 정의 반환 값 대의원 예제에 설명 된 Custom_Fake 필드입니다.
헬퍼 변수 (예 : gettime_custom_now)를 사용하여 해당 출력을 검색하여 선택적으로 출력을 생성하려면 사용자 정의 함수 (예 : gettime_custom_fake)를 작성해야합니다. 그런 다음 모든 창의성을 함께 묶습니다. 가장 중요한 부분 (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 );
} fff 함수 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 ++의 전체 길이 예제에 대한 예제 디렉토리 아래를 살펴보십시오. 테스트 디렉토리 아래 프레임 워크에 대한 테스트 스위트도 있습니다.
그래서 요점은 무엇입니까?
| 매크로 | 설명 | 예 |
|---|---|---|
| 가짜_void_func (fn [, arg_types*]); | fn이라는 가짜 함수 정의 n 인수와 함께 void를 반환합니다. | 가짜_void_func (display_output_message, const char*); |
| 가짜_value_func (return_type, fn [, arg_types*]); | return_type 유형으로 값을 반환하는 가짜 함수 정의 n 인수 | 가짜_value_func (int, display_get_line_insert_index); |
| 가짜_void_func_vararg (fn [, arg_types*], ...); | return_type 유형으로 void를 반환하는 가짜 variadic 함수 정의 n 인수와 n variadic 인수를 정의하십시오. | 가짜_void_func_vararg (fn, const char*, ...) |
| 가짜_value_func_vararg (return_type, fn [, arg_types*], ...); | return_type 유형을 사용하여 값을 반환하는 가짜 변수 함수 정의 n 인수 및 n variadic 인수 | 가짜_value_func_vararg (int, fprintf, file*, const char*, ...) |
| reset_fake (fn); | fn이라는 가짜 함수의 상태를 재설정하십시오 | reset_fake (display_init); |