実装
test.h
#ifdef TEST
// テスト実行ではもとのmain関数は無効化
#define main main_
#define ARGS_0
#define ARGS_1 t->a1
#define ARGS_2 ARGS_1, t->a2
#define ARGS_3 ARGS_2, t->a3
#define ARGS_4 ARGS_3, t->a4
// テストできる関数の引数の数は4個まで
#define MEM_DEF_1(_1) _1 a0;
#define MEM_DEF_2(_1, _2) MEM_DEF_1(_1) _2 a1;
#define MEM_DEF_3(_1, _2, _3) MEM_DEF_2(_1, _2) _3 a2;
#define MEM_DEF_4(_1, _2, _3, _4) MEM_DEF_3(_1, _2, _3) _4 a3;
#define MEM_DEF_5(_1, _2, _3, _4, _5) MEM_DEF_4(_1, _2, _3, _4) _5 a4;
#define GET_M(_1, _2, _3, _4, _5, NAME, ...) NAME
#define EXPAND(...) __VA_ARGS__
#define DO_FN(fn, ...) \
fn(GET_M(__VA_ARGS__, ARGS_4, ARGS_3, ARGS_2, ARGS_1, ARGS_0))
#define SIGNATURE(...) \
struct { \
GET_M(__VA_ARGS__, MEM_DEF_5, MEM_DEF_4, MEM_DEF_3, MEM_DEF_2, MEM_DEF_1) \
(__VA_ARGS__) \
}
// 引数のsignatureは (int, char, bool) のようなフォーマット
// つまり
// SIGNATURE signature
// は
// SIGNATURE (int, char, bool)
// に展開される。EXPAND signature も同じ要領
#define table_driven_test(name, fn, signature, ...) \
typedef SIGNATURE signature sigstruct##name; \
__attribute__((constructor)) void tabledriventest##name() { \
sigstruct##name data[] = __VA_ARGS__; \
printf("Testing " #name "... => "); \
for (size_t i = 0; i < sizeof(data) / sizeof(data[0]); i++) { \
sigstruct##name *t = data + i; \
typeof(t->a0) result = DO_FN(fn, EXPAND signature); \
if (!eq(result, t->a0)) { \
printf("[NG] Test case %zu failed: expected ", i); \
print(t->a0); \
printf(", but got "); \
print(result); \
puts(""); \
return; \
} \
} \
puts("[OK]"); \
}
#else
#define table_driven_test(...)
#endif
test.c
#ifdef TEST
int main() {}
#endif
コード内のeq()
とかprint()
はマクロの_Generic
とかattribute
のoverloadable
とかで実現してください
テストケースが失敗したときは、
- テストケースの番号
- 予想の返り値
- 実際の返り値
が出力されます
使い方
ソースと一緒にtest.c
もビルド
テストを有効にするときはコンパイルオプションで-DTEST
を追加
table_driven_test (
テストの名前,
テストする関数の名前,
(関数の返り値の型, 引数1の型, 引数2の型, ...),
{
{ 予想の返り値1, 引数1_1, 引数1_2, ...},
{ 予想の返り値2, 引数2_1, 引数2_2, ...},
...
}
)
例
example.c
#include "test.h"
#include <stdio.h>
#include <math.h>
int main() {}
double avg_score(int math, int jap, int eng) {
if (math < 0 || 100 < math || jap < 0 || 100 < jap || eng < 0 || 100 < eng)
return -1.0;
return (math + jap + eng) / 3.0;
}
// テスト対象の関数↑の返り値の型がdoubleだからeqとprintもdouble型のが必要
__attribute__((overloadable)) _Bool eq(double lhs, double rhs) {
return fabs(lhs - rhs) < 0.01;
}
// eqとかprintは他のファイルに分割してtest.hでincludeした方が見栄えがいい
// 他の型を追加するには
// __attribute__((overloadable)) _Bool eq(int lhs, int rhs) {
// return lhs == rhs;
// }
__attribute__((overloadable)) void print(double x) {
printf("%lf", x);
}
// __attribute__((overloadable)) void print(int x) {
// printf("%d", x);
// }
table_driven_test (
calculate_average_score, // テストの名前
avg_score, // テスト対象の関数
(double, int, int, int), // (関数の返り値の型, 引数の型)
{ // テストケースの配列
{-1.0, 101, 100, 100},
{-1.0, -1, 0, 0},
{50.0, 0, 50, 100},
{100.0, 100, 100, 100},
{99.0, 100, 99, 98},
{0.0, 0, 0, 0}
} // 全部成功するはず
)
コンパイル
$ # example.c, test.{c,h}をカレントディレクトリに用意
$ clang example.c test.c -DTEST # gccも可