0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiita全国学生対抗戦Advent Calendar 2024

Day 21

マクロでテーブル駆動テスト

Posted at

実装

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とかattributeoverloadableとかで実現してください
テストケースが失敗したときは、

  • テストケースの番号
  • 予想の返り値
  • 実際の返り値

が出力されます

使い方

ソースと一緒に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も可
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?