はじめに
Zig言語では, テストを以下のように書ける.
test "test name" {
...
}
Cでは標準のテスト構文がないからテストを書くのがそこそこ面倒である.
そこで, CでもZigくらい簡潔に書けるテスト構文をマクロで実装した.
機能
-
test
ブロックの最後に到達したらテスト成功 -
expect(cond)
で条件をチェックでき,false
の場合テスト失敗 -
unreachable
で到達してはいけない箇所を明示でき, そこに到達したらテスト失敗
目指す形.c
int add(int x, int y) { return x + y; }
test(add_function_test) {
expect(add(4, 5) == 9);
expect(add(9, 2) == 11);
int x = 15, y = 30;
expect(add(x, y) == x + y);
}
test(unreachable_example) {
for (;;) {
break;
unreachable;
}
}
ソースコード
説明は後
testing.h
#include <setjmp.h>
#ifdef TEST_MODE
extern int test_success_count;
extern int test_failed_count;
// test() マクロ
#define test(name) \
void test##name(jmp_buf); \
__attribute__((constructor)) void testrunner##name() { \
jmp_buf jb; \
if (setjmp(jb) == 0) \
test##name(jb); \
else { \
puts(#name ": [NG]"); \
test_failed_count++; \
return; \
} \
puts(#name ": [OK]"); \
test_success_count++; \
} \
void test##name(jmp_buf jb)
#else
#define test(name) void test_dummy##name(jmp_buf jb)
#endif
// expect() マクロ
#define expect(cond) \
if (!(cond)) { \
printf("Failed at %s:%d " #cond " ", __FILE__, __LINE__); \
longjmp(jb, 1); \
}
// unreachable マクロ
#define unreachable \
do { \
printf("Reached line %s:%d", __FILE__, __LINE__); \
longjmp(jb, 1); \
} while (0)
testing.c
#include "testing.h"
#include <stdio.h>
#ifdef TEST_MODE
#undef main
// 処理はconstructorとdestructorにやらせるからmain関数は空
int main() {}
// 他のファイルにあるであろうもともとのmain関数を無効化
#define main main_
int test_success_count;
int test_failed_count;
// テスト結果の集計を出力
__attribute__((destructor)) void report_test_result() {
printf("\nPassed: %d/%d\n", test_success_count, test_success_count + test_failed_count);
}
#endif
使い方
testing.h
をinclude
テストを有効化するときは-DTEST_MODE
をつけてtesting.c
もいっしょにビルド
説明
setjmpを使う理由
当初はtest##name()
でのテストの結果をtestrunner##name()
に返すときbool
の返り値で返そうと思ってたけどそれだと
test(name) {
...
return true;
}
みたいにブロックの終わりに毎回return
を書く必要があり, それは冗長でかっこ悪いと思ったからlongjmp
を使って結果を返すことにした. これにより,
-
test
ブロックの最後に到達 → 成功 - 途中で
longjmp
が呼ばれる → 失敗
のように, Zigに近い構文を実現した.
longjmp
を使う場面はマクロで隠蔽してあるから構造化プログラミングの観点からも問題ない(多分).
テスト自動実行について
testrunner##name()
constructor
属性がついてるので定義だけしておけばプログラム開始時に勝手に呼ばれる.
report_test_result()
destructor
属性がついてるので確実にすべてのテストが終わった後に勝手に呼ばれる.
エラー報告について
expect()
, unreachable
は失敗したとき
- 現在のファイル名
- 現在の行数
- 条件式(
expect()
の場合)
を表示してからlongjmp
する.