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を返す関数を定義するマクロが拡張されます。また、偽物に関するすべての情報を含むstruct "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 ;OK、おもちゃの例で十分です。議論で機能を偽造するのはどうですか?
// 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を呼び出し、次にfunctionaを再度テストしたいとしますか?まあ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 ]);
}呼び出しが削除されたかどうかを調べるには、2つの方法があります。 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 );
}もう1つは、コールカウントが履歴サイズよりも大きいかどうかを確認することです。
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でこれを行う1つの方法は、偽の関数に対して一連の戻り値を指定することです。例で説明する方がおそらく簡単です。
// 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」、2番目の呼び出しの値「Y」、3番目の呼び出しのoutパラメーターへの値「z」を設定します。 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マクロによく似ています。
2つの関数F1とF2があるとします。 F2によって割り当てられたリソースをリリースするには、F2を呼び出す必要がありますが、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フィールドの返された値にアクセスします。
Macros FAKE_VALUE_FUNC_VARARGおよびFAKE_VOID_FUNC_VARARGを使用して、variadic関数を偽造できます。例えば:
FAKE_VALUE_FUNC_VARARG(int, fprintf, FILE *, const char*, ...);
カスタム偽の関数からバリアードパラメーターにアクセスするために、 va_listパラメーターを宣言します。たとえば、 fprintf()のカスタム偽物は、次のようなreal 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);
}
Return Value Delegatesと同様に、 SET_CUSTOM_FAKE_SEQを使用して、Variadic関数のシーケンスを指定することもできます。例については、テストファイルを参照してください。
FFFは、MicrosoftのVisual C/C ++呼び出しコンベンションの仕様を有効にする機能が限られていますが、FFFのヘッダーファイルfff.hを生成するときにこのサポートを有効にする必要があります。
ruby fakegen.rb --with-calling-conventions > fff.hこのサポートを有効にすることにより、FFFの偽の関数の足場はすべて、各値の__cdeclまたはvoid fakeの呼び出し条約の仕様を必要とします。
いくつかの基本的な例を次に示します。指定されている通話条約の配置は、偽物が空白または値関数であるかによって異なることに注意してください。
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 ++ラムダを使用する場合、これをすべて単一のテスト関数に組み合わせることで、テストのすべての詳細を簡単に監督できます。
#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 , ...);FFF_GCC_FUNCTION_ATTRIBUTESディレクティブを使用して、偽物のGCC関数属性を指定できます。
1つのUSFUL属性は、リンク時に非ウィークバリアントによってオーバーライドできるように関数をマークする弱い属性です。 FFFと組み合わせて弱い機能を使用すると、テストアプローチを簡素化するのに役立ちます。
例えば:
あなたはすべての偽物をSOのような弱い属性でマークすることができます:
#define FFF_GCC_FUNCTION_ATTRIBUTES __attribute__((weak))
#include "fff.h"
上記のアプローチを示すプロジェクトの例: ./examples/ weak_linkingを参照してください。
C ++の両方で、完全な長さの例については、Examplesディレクトリの下をご覧ください。テストディレクトリの下にあるフレームワークのテストスイートもあります。
それで、ポイントは何ですか?
| マクロ | 説明 | 例 |
|---|---|---|
| fake_void_func(fn [、arg_types*]); | N引数でVoidを返すFNという名前の偽の関数を定義する | 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*]、...); | 型return_typeがn引数とn variadic引数を取得してvoidを返す偽のvariadic関数を定義する | fake_void_func_vararg(fn、const char*、...) |
| fake_value_func_vararg(return_type、fn [、arg_types*]、...); | 偽のバリアード関数を定義して、type return_typeがn引数とn variadic引数を取得して値を返します | fake_value_func_vararg(int、fprintf、file*、const char*、...) |
| reset_fake(fn); | FNと呼ばれる偽の機能の状態をリセットします | reset_fake(display_init); |