関数インジェクションを利用した単体テストで利用できる関数マクロ
私は組込み開発でC言語を利用している。
組込み開発は一癖も二癖もあるような開発にどうしてもなってしまう。
そのような開発において、次のような単体テストにまつわる対処をどのように行うのかが重要になってくると思われる。
- ハードウェア依存のコードをどのようにテストするか(WiFiモジュールの初期化関数など)
- Free RTOSなどのC言語標準の関数以外が混じるような関数をどのようにテストするか
関数を外から注入することでこれらを解決する。
この記事では、それを関数インジェクションと呼ぶことにする。
これは、依存性の注入だったりDIパターンから着想を得ている。
開発環境
- Visual Studio Code
- gcc Apple LLVM version 10.0.1 (clang-1001.0.46.4)
正直なところ、内容がC言語の標準的な機能しか利用していないので、バージョンとかを合わせずとも問題ないと思われる。
関数をインジェクションする方法
関数ポインタを利用した構造体を作成し、適当に挿入し関数を実際に利用するところまで記述する。
typedef struct
{
void (*Func1)(void);
int (*Func2)(int a);
} Test_t;
void func1(void) {}
int func2(int a) { return a; }
int FUNC(Test_t test)
{
test.Func1();
printf("%d\r\n", test.Func2(2));
}
int main()
{
Test_t test;
test.Func1 = &func1;
test.Func2 = &func2;
FUNC(test);
return 0;
}
念の為、解説を行う。
- main関数で、Test_t型の構造体を作成
- 作成した構造体をFUNCの引数とする
- FUNCは受け取ったTest_t型構造体のFunc1を利用している。
こうすることで、FUNCの中で利用するFunc1をmain関数内で置き換えられる。
これでどういう形で関数ポインタを渡したいかを理解してもらえたと思う。
関数マクロ紹介と使い方
早速紹介を行っていきたいと思う。
実装の関係上、関数の返却値の有無でマクロが別れている。
共通の構造体を先に紹介しておく。
/**
* @brief 関数を呼び出したか判定用の列挙型
*/
typedef enum
{
/**
* @brief 呼び出していない
*/
mock_DEFINE_RESULT_NOT_CALL = 0,
/**
* @brief 呼び出した
*/
mock_DEFINE_RESULT_CALL = 1,
} MOCK_DEFINE_RESULT_t;
関数の返却値がない場合
/**
* @brief ダミーしたい関数名を生成する
* @param[in] name モック関数の名前
*/
# define V_FUNC_NAME(name) v##name##MacroMOCK
/**
* @brief FUNC_NAMEの呼び出し有無を保存するフラグの変数名を生成する
* @param[in] name モック関数の名前
*/
# define V_VARIABLE_NAME(name) gx##name##CallFlag
/**
* @brief V_FUNC_NAMEの呼び出し回数を保存するフラグの変数名を生成する
* @param[in] name モック関数の名前
*/
# define V_VARIABLE_TIMES_NAME(name) gx##name##MacroCallTimesFlag
/**
* @brief V_FUNC_NAMEの呼び出し有無を保存するフラグを取得する関数名を生成する
* @param[in] name モック関数の名前
*/
# define V_GET_FUNC_NAME(name) xGet##name##MacroFlag
/**
* @brief V_FUNC_NAMEの呼び出し回数を保存するフラグを取得する関数名を生成する
* @param[in] name モック関数の名前
*/
# define V_GET_FUNC_TIMES_NAME(name) xGet##name##MacroTimesFlag
# include "mock_macro.h"
# include "mock_macro_v_func_name.h"
/**
* @brief V_MOCK_FUNCで定義した変数の初期化をする
* @details
* Testファイルの[関数マクロを利用したモック関数の初期化]ブロックで利用すること
* @param[in] name モック関数の名前
*/
# define V_INIT_VARIABLE_FUNC(name) \
V_VARIABLE_NAME(name) = MOCK_DEFINE_RESULT_NOT_CALL; \
V_VARIABLE_TIMES_NAME(name) = 0
/**
* @brief モック関数を定義[返却される型が無い関数]
* @details
* Testファイルの[関数マクロを利用したモック関数定義]ブロックで利用すること
* このマクロで定義した関数郡の説明
* V_MOCK_FUNC_CALL(name, ...)
* このマクロを直接利用することはない。実際の関数はインジェクションを返して実行される。
*
* V_GET_FUNC_POINTER(name)
* V_MOCK_FUNC_CALLの関数ポインタを取得するマクロ
*
* V_GET_MOCK_FUNC_CALL(name)
* V_MOCK_FUNC_CALLの関数の呼び出し有無を確認するためのマクロ
*
* V_GET_MOCK_FUNC_TIMES_CALL(name)
* V_MOCK_FUNC_CALLの関数の呼び出し回数を確認するためのマクロ
*
* @param[in] name モック関数の名前
* @param[in] ... モック関数の必要な型と仮引数
*/
# define V_MOCK_FUNC_DEFINITION(name, ...) \
static MOCK_DEFINE_RESULT_t V_VARIABLE_NAME(name); \
static uint32_t V_VARIABLE_TIMES_NAME(name); \
static void V_FUNC_NAME(name)(__VA_ARGS__); \
static void V_FUNC_NAME(name)(__VA_ARGS__) \
{ \
V_VARIABLE_NAME(name) = MOCK_DEFINE_RESULT_CALL; \
V_VARIABLE_TIMES_NAME(name) \
++; \
} \
static MOCK_DEFINE_RESULT_t V_GET_FUNC_NAME(name)(void); \
static MOCK_DEFINE_RESULT_t V_GET_FUNC_NAME(name)(void) \
{ \
return V_VARIABLE_NAME(name); \
} \
static uint32_t V_GET_FUNC_TIMES_NAME(name)(void); \
static uint32_t V_GET_FUNC_TIMES_NAME(name)(void) \
{ \
return V_VARIABLE_TIMES_NAME(name); \
}
/**
* @brief V_MOCK_FUNCのモック関数を呼び出す
* @param[in] name モック関数の名前
* @param[in] ... モック関数の必要な引数
*/
# define V_MOCK_FUNC_CALL(name, ...) \
V_FUNC_NAME(name) \
(__VA_ARGS__)
/**
* @brief モック関数を呼び出したかのフラグを取得する
* @param[in] name モック関数の名前
*/
# define V_GET_MOCK_FUNC_CALL(name) \
V_GET_FUNC_NAME(name) \
()
/**
* @brief モック関数の何回呼び出し回数のフラグを取得する
* @param[in] name モック関数の名前
*/
# define V_GET_MOCK_FUNC_TIMES_CALL(name) \
V_GET_FUNC_TIMES_NAME(name) \
()
/**
* @brief モック関数の関数ポインタを取得
* @param[in] name モック関数の名前
*/
# define V_GET_FUNC_POINTER(name) \
&V_FUNC_NAME(name)
関数の返却値がある場合
/**
* @brief FUNC_NAMEの呼び出し有無を保存するフラグの変数名を生成する
* @param[in] name モック関数の名前
*/
# define X_VARIABLE_NAME(name) gx##name##CallFlag
/**
* @brief FUNC_NAMEの呼び出し回数を保存するフラグの変数名を生成する
* @param[in] name モック関数の名前
*/
# define X_VARIABLE_TIMES_NAME(name) gx##name##CallTimesFlag
/**
* @brief FUNC_NAMEを失敗させるためのフラグ名を生成する
* @param[in] name モック関数の名前
*/
# define X_VARIABLE_NAME_FAIL_FLAG(name) gx##name##FailFlag
/**
* @brief X_FUNC_NAMEの関数の返却データが格納されている配列
* @param[in] name モック関数の名前
*/
# define X_FUNC_RESULT_DATA_NAME(name) gx##name##ResultData
/**
* @brief X_FUNC_RESULT_DATA_NAMEの配列にデータを設定する関数名
* @param[in] name モック関数の名前
*/
# define X_SET_RESULT_DATA_FUNC_NAME(name) vSet##name##ResultData
/**
* @brief X_SET_RESULT_DATA_FUNC_NAMEを呼び出したかのフラグ名
* @param[in] name モック関数の名前
*/
# define X_SET_RESULT_DATA_FUNC_CALL_FLAG_NAME(name) gx##name##ResultDataCallFlag
/**
* @brief X_FUNC_NAMEの呼び出し有無のフラグを取得する関数名を生成する
* @param[in] name モック関数の名前
*/
# define X_GET_FUNC_NAME(name) xGet##name##MacroFlag
/**
* @brief X_FUNC_NAMEの呼び出し回数のフラグを取得する関数名を生成する
* @param[in] name モック関数の名前
*/
# define X_GET_FUNC_TIMES_NAME(name) xGet##name##MacroTimesFlag
/**
* @brief ダミーしたい関数名を生成する
* @param[in] name モック関数の名前
*/
# define X_FUNC_NAME(name) x##name##MacroMOCK
/**
* @brief X_GET_FUNC_NAMEを失敗させる関数名を生成する
* @param[in] name モック関数の名前
*/
# define X_FUNC_FAIL_NAME(name) v##name##FailMacroMOCK
/**
* @brief X_FUNC_RESULT_DATA配列の最大サイズ
*/
# define X_FUNC_RESULT_DATA_LENGTH (5)
# include "mock_macro.h"
# include "mock_macro_x_func_name.h"
/**
* @brief V_MOCK_FUNCで定義した変数の初期化をする
* @details
* Testファイルの[関数マクロを利用したモック関数の初期化]ブロックで利用すること
* @param[in] name モック関数の名前
*/
# define X_INIT_VARIABLE_FUNC(name) \
X_VARIABLE_NAME(name) = MOCK_DEFINE_RESULT_NOT_CALL; \
X_VARIABLE_NAME_FAIL_FLAG(name) = MOCK_DEFINE_RESULT_NOT_CALL; \
X_SET_RESULT_DATA_FUNC_CALL_FLAG_NAME(name) = MOCK_DEFINE_RESULT_NOT_CALL; \
X_VARIABLE_TIMES_NAME(name) = 0; \
memset(X_FUNC_RESULT_DATA_NAME(name), 0, sizeof(X_FUNC_RESULT_DATA_NAME(name)))
/**
* @brief モック関数を定義[返却される型がある関数]
* @details
* Testファイルの[関数マクロを利用したモック関数定義]ブロックで利用すること
* このマクロで定義した関数郡の説明
* X_MOCK_FUNC_CALL(name, ...)
* このマクロを直接利用することはない。実際の関数はインジェクションを返して実行される。
* この関数の返却値の下記の条件に従う
* 優先度 高
* 1. X_SET_FUNC_RESULT_CALLでデータを定義している場合、関数の呼び出し回数に応じてデータを返却する
* 2. X_MOCK_FUNC_FAILを事前に呼び出している場合、定義時に指定しているFalseのデータが返却される
* 3. 上記のどれでも無い場合、定義時に指定しているTrueのデータが返却される
* 優先度 低
*
* X_GET_FUNC_POINTER(name)
* X_MOCK_FUNC_CALLの関数ポインタを取得するマクロ
*
* X_GET_MOCK_FUNC_CALL(name)
* X_MOCK_FUNC_CALLの関数を呼び出したか確認するためのマクロ
*
* X_GET_MOCK_FUNC_TIMES_CALL(name)
* X_MOCK_FUNC_CALLの関数の呼び出し回数を確認するためのマクロ
*
* X_MOCK_FUNC_FAIL(name)
* X_MOCK_FUNC_CALLの関数を失敗させるためのマクロ
*
* X_SET_FUNC_RESULT_CALL(name, index, result)
* X_MOCK_FUNC_CALLの関数の返却値を設定するためのマクロ
* indexは0オリジンで最大で4まで指定することができる
*
* @param[in] name モック関数の名前
* @param[in] type モック関数の返却型
* @param[in] trueValue 成功時の返却値
* @param[in] falseValue 失敗時の返却値
* @param[in] ... モック関数の必要な型と仮引数
*/
# define X_MOCK_FUNC_DEFINITION(name, type, trueValue, falseValue, ...) \
static MOCK_DEFINE_RESULT_t X_VARIABLE_NAME(name); \
static MOCK_DEFINE_RESULT_t X_VARIABLE_NAME_FAIL_FLAG(name); \
static MOCK_DEFINE_RESULT_t X_SET_RESULT_DATA_FUNC_CALL_FLAG_NAME(name); \
static uint32_t X_VARIABLE_TIMES_NAME(name); \
static type X_FUNC_RESULT_DATA_NAME(name)[X_FUNC_RESULT_DATA_LENGTH]; \
static type X_FUNC_NAME(name)(__VA_ARGS__); \
static type X_FUNC_NAME(name)(__VA_ARGS__) \
{ \
X_VARIABLE_NAME(name) = MOCK_DEFINE_RESULT_CALL; \
type xResult = (X_VARIABLE_NAME_FAIL_FLAG(name) == MOCK_DEFINE_RESULT_NOT_CALL) ? trueValue : falseValue; \
xResult = (X_SET_RESULT_DATA_FUNC_CALL_FLAG_NAME(name) == MOCK_DEFINE_RESULT_CALL) ? X_FUNC_RESULT_DATA_NAME(name)[X_VARIABLE_TIMES_NAME(name)] : xResult; \
X_VARIABLE_TIMES_NAME(name) \
++; \
return xResult; \
} \
static MOCK_DEFINE_RESULT_t X_GET_FUNC_NAME(name)(void); \
static MOCK_DEFINE_RESULT_t X_GET_FUNC_NAME(name)(void) \
{ \
return X_VARIABLE_NAME(name); \
} \
static void X_FUNC_FAIL_NAME(name)(void); \
static void X_FUNC_FAIL_NAME(name)(void) \
{ \
X_VARIABLE_NAME_FAIL_FLAG(name) = MOCK_DEFINE_RESULT_CALL; \
} \
static uint32_t X_GET_FUNC_TIMES_NAME(name)(void); \
static uint32_t X_GET_FUNC_TIMES_NAME(name)(void) \
{ \
return X_VARIABLE_TIMES_NAME(name); \
} \
static void X_SET_RESULT_DATA_FUNC_NAME(name)(uint32_t uxIndex, type xData); \
static void X_SET_RESULT_DATA_FUNC_NAME(name)(uint32_t uxIndex, type xData) \
{ \
X_SET_RESULT_DATA_FUNC_CALL_FLAG_NAME(name) = MOCK_DEFINE_RESULT_CALL; \
X_FUNC_RESULT_DATA_NAME(name) \
[uxIndex] = xData; \
}
/**
* @brief X_MOCK_FUNCのモック関数を呼び出す
* @param[in] name モック関数の名前
* @param[in] ... モック関数の必要な引数
*/
# define X_MOCK_FUNC_CALL(name, ...) \
X_FUNC_NAME(name) \
(__VA_ARGS__)
/**
* @brief モック関数を呼び出したかのフラグを取得する
* @param[in] name モック関数の名前
*/
# define X_GET_MOCK_FUNC_CALL(name) \
X_GET_FUNC_NAME(name) \
()
/**
* @brief モック関数の呼び出し回数のフラグを取得する
* @param[in] name モック関数の名前
*/
# define X_GET_MOCK_FUNC_TIMES_CALL(name) \
X_GET_FUNC_TIMES_NAME(name) \
()
/**
* @brief モック関数を失敗させる関数を呼び出す
* @param[in] name モック関数の名前
*/
# define X_MOCK_FUNC_FAIL(name) \
X_FUNC_FAIL_NAME(name) \
()
/**
* @brief モック関数の返却値を設定する関数を呼び出す
* @param[in] name モック関数の名前
* @param[in] index 何回目に返却したいのか(0オリジン)
* @param[in] result 返却したいデータ
*/
# define X_SET_FUNC_RESULT_CALL(name, index, result) \
X_SET_RESULT_DATA_FUNC_NAME(name) \
(index, result)
/**
* @brief モック関数の関数ポインタを取得
* @param[in] name モック関数の名前
*/
# define X_GET_FUNC_POINTER(name) \
&X_FUNC_NAME(name)
使い方
基本的には関数の返却値の有り無しに使い方の違いはあまりなく、共通のところから解説を行っていく。
また、関数の詳細な使い方はそれぞれのマクロで記述する。
共通
- ダミー関数を作成
V_MOCK_FUNC_DEFINITION
、X_MOCK_FUNC_DEFINITION
- ダミー関数の関数ポインタを取得
V_GET_FUNC_POINTER
、X_GET_FUNC_POINTER
- ダミー関数で利用している変数群の初期化
V_INIT_VARIABLE_FUNC
、X_INIT_VARIABLE_FUNC
- ダミー関数の呼び出し有無のフラグを取得
V_GET_MOCK_FUNC_CALL
、X_GET_MOCK_FUNC_CALL
- ダミー関数の呼び出し回数を取得
V_GET_MOCK_FUNC_TIMES_CALL
、X_GET_MOCK_FUNC_TIMES_CALL
# include "stdio.h"
# include "mock_macro.h"
# include "mock_macro_v_func.h"
# include "mock_macro_x_func.h"
typedef struct
{
void (*Func1)(void);
int (*Func2)(int a);
} Test_t;
void func1(void) { printf("func1 \r\n"); }
int func2(int a) { return a; }
// MOCK_FUNC_DEFINITIONで関数の定義を行うため、main関数の上で行う定義する必要がある
V_MOCK_FUNC_DEFINITION(FUNC1, void);
X_MOCK_FUNC_DEFINITION(FUNC2, int, 1, 0, int a);
int func(Test_t test)
{
test.Func1();
printf("%d\r\n", test.Func2(2));
}
int main()
{
// 利用する前にINIT_VARIABLE_FUNCで初期化する
// 本来はunit testで利用するものなので、before eachなどで利用することになる
V_INIT_VARIABLE_FUNC(V_TEST);
X_INIT_VARIABLE_FUNC(X_TEST);
// GET_FUNC_POINTERでMOCK_FUNC_DEFINITIONで定義した関数のポインタを取得することができる
Test_t mock;
mock.Func1 = V_GET_FUNC_POINTER(FUNC1);
mock.Func2 = X_GET_FUNC_POINTER(FUNC2);
func(mock);
printf("FUNC1 Call: %s \r\n", MOCK_DEFINE_RESULT_CALL == V_GET_MOCK_FUNC_CALL(FUNC1) ? "TRUE" : "FALSE");
printf("FUNC1 Call Times: %d \r\n", V_GET_MOCK_FUNC_TIMES_CALL(FUNC1));
printf("FUNC2 Call: %s \r\n", MOCK_DEFINE_RESULT_CALL == X_GET_MOCK_FUNC_CALL(FUNC2) ? "TRUE" : "FALSE");
printf("FUNC2 Call Times: %d \r\n", X_GET_MOCK_FUNC_TIMES_CALL(FUNC2));
return 0;
}
上記の例では、func1
関数のモックをFUNC1
という名前で作成している。
本来の関数を使いたい時はTest_t test
のように正しい関数の関数ポインタを渡す。
テストのためにモック関数を渡すときにはTest_t mock
のようにする必要がある。
GET_FUNC_POINTER‘系のマクロを利用することで関数ポインタを取得することができる。
GET_MOCK_FUNC_CALLや
GET_MOCK_FUNC_TIMES_CALL`を利用することで関数のコール有無とコール回数を取得することができる。
これで、インジェクションした関数を規定回数呼び出したかなどを評価することができる。
返却値がある場合のみ
- ダミー関数を失敗させる場合
X_MOCK_FUNC_FAIL
- ダミー関数の返却値を呼び出し回数に応じて変更させたい場合
X_SET_FUNC_RESULT_CALL
# include "stdio.h"
# include "mock_macro.h"
# include "mock_macro_v_func.h"
# include "mock_macro_x_func.h"
typedef struct
{
int (*Func2)(int a);
} Test_t;
int func2(int a) { return a; }
// MOCK_FUNC_DEFINITIONで関数の定義を行うため、main関数の上で行う定義する必要がある
X_MOCK_FUNC_DEFINITION(FUNC2, int, 1, 0, int a);
int func(Test_t test)
{
printf("%d\r\n", test.Func2(2));
}
int main()
{
// 利用する前にINIT_VARIABLE_FUNCで初期化する
// 本来はunit testで利用するものなので、before eachなどで利用することになる
X_INIT_VARIABLE_FUNC(X_TEST);
// GET_FUNC_POINTERでMOCK_FUNC_DEFINITIONで定義した関数のポインタを取得することができる
Test_t mock;
mock.Func2 = X_GET_FUNC_POINTER(FUNC2);
# if 0
// FUNC2のモック関数が呼び出される前に、X_MOCK_FUNC_FAILを呼び出すことで、関数を失敗させることができる
X_MOCK_FUNC_FAIL(FUNC2);
# else
// 失敗させない代わりに、呼び出される回数に応じて、返却される値を変更することもできる
X_SET_FUNC_RESULT_CALL(FUNC2, 0, 1);
X_SET_FUNC_RESULT_CALL(FUNC2, 1, 10);
X_SET_FUNC_RESULT_CALL(FUNC2, 2, 100);
X_SET_FUNC_RESULT_CALL(FUNC2, 3, 1000);
X_SET_FUNC_RESULT_CALL(FUNC2, 4, 10000);
# endif
func(mock);
printf("FUNC2 Call: %s \r\n", MOCK_DEFINE_RESULT_CALL == X_GET_MOCK_FUNC_CALL(FUNC2) ? "TRUE" : "FALSE");
printf("FUNC2 Call Times: %d \r\n", X_GET_MOCK_FUNC_TIMES_CALL(FUNC2));
return 0;
}
上記のように事前にX_MOCK_FUNC_FAIL
を呼び出すことによって、関数の定義時に指定した変数を返却することができる。
また、X_SET_FUNC_RESULT_CALL
を利用することによって、呼び出し回数に応じた返却値の設定を行うことができる。
このマクロで気を付けないといけないのはX_MOCK_FUNC_FAIL
とX_SET_FUNC_RESULT_CALL
には優先順位があり、次の順番でデータが返却されることである。
-
X_SET_FUNC_RESULT_CALL
でデータを定義している場合、関数の呼び出し回数に応じてデータを返却する -
X_MOCK_FUNC_FAIL
を事前に呼び出している場合、定義時に指定しているFalseのデータが返却される - 上記のどれでも無い場合、定義時に指定しているTrueのデータが返却される