ヘッダーオンリーのC言語向け単体テストフレームワークCUTEを作った

C99以降向けのヘッダーオンリーで動作する単体テストフレームワークCUTEを作成しました。

github.com

モチベーション

C言語のコードに対する単体テストを書く際、ごく簡単なものであれば assert() を使用することが多いと思います。

しかし、テスト対象が複雑であったり、テストの分量が多い場合には assert() のみでは機能不足を感じることが今まで多々ありました。

そこで、自分用に必要十分な機能を備え、なおかつシンプルな使い心地の単体テストフレームワークを作成しました。

動作環境

C89以前、およびMSVCはサポートしていません。

使用方法

CUTEはヘッダーオンリーのライブラリであるため、適当なディレクトリに cute.h をダウンロードし、includeする だけで使用できます。

また、CMakeを使用している場合はFetchContentを使用することで以下のようにプロジェクトをインポート出来ます。

include(FetchContent)

FetchContent_Declare(cute
    GIT_REPOSITORY "https://github.com/Ryooooooga/cute.git"
    GIT_TAG        "main"
)

FetchContent_MakeAvailable(cute)

target_link_library(your_project cute)

コード例

CUTEを使用した簡単なテストの記述例を以下に示します。

#include "cute.h"

int factorial(int n) {
    if (n > 0) {
        return n * factorial(n - 1);
    }
    return 1;
}

TEST(factorial) {
    EXPECT(factorial(0), eq(1));
    EXPECT(factorial(1), eq(1));
    EXPECT_MSG(factorial(5), eq(120), "5! == 120 (actual %d)", factorial(5));
}

TEST(string) {
    const char *s = "Hello, world!";

    ASSERT(s, is_not_null);
    EXPECT(s, eq_str("Hello, world!"));
    EXPECT((s, 4), eq_str_n("Hell"));
    EXPECT(s, contains("world"));
    EXPECT(s, not(contains("nya")));
}

int main(void) {
    RUN_TESTS() {
        RUN(factorial);
        RUN(string);
        return DUMP_RESULT();
    }
}

その他の例は example.c で確認できます。

基本的な書き方

上記の通り、基本的な条件は以下のような形式で記述します。

  • EXPECT(実際の値, 述語);
  • EXPECT_MSG(実際の値, 述語, メッセージ);
  • ASSERT(実際の値, 述語);
  • ASSERT_MSG(実際の値, 述語, メッセージ);

EXPECT()は条件が満たされなかった場合も以降のテストが継続されるのに対して、ASSERT()は即座にエラーの発生したテストを中断します *1

述語

現在のところ、値に対する述語として以下のようなものが用意されています。

  • is_true: actual == true
  • is_false: actual == false
  • is_null: actual == NULL
  • is_not_null: actual != NULL
  • eq(x): actual == x
  • ne(x): actual != x
  • lt(x): actual < x
  • le(x): actual <= x
  • gt(x): actual > x
  • ge(x): actual >= x
  • eq_str(s): actual == s
  • eq_str_n(s): actual[0..n] == s ※ このeq_str_n() のみ EXPECT((ptr, len), eq_str_n(s)) のように記述します。
  • contains(s): actual が s を含む
  • not(pred): 述語predを反転する

また、述語そのものはマクロや関数ではないため、以下のように同名の変数や関数が定義されていても問題なく動作します。

bool is_true = true;
EXPECT(is_true, is_true);

int eq = 42;
EXPECT(42, eq(eq));

グルーピング

GROUP() を使用することでテストコードをグルーピングすることが出来ます。

TEST(numeric) {
    GROUP("integer") {
        int n = 42;

        EXPECT(n, eq(42));
        EXPECT(n, ne(41));
        EXPECT(n, lt(43));
        EXPECT(n, le(42));
        EXPECT(n, gt(41));
        EXPECT(n, ge(42));
    }

    GROUP("floating point") {
        double f = 3.14159265;

        EXPECT(f, ne(0));
        EXPECT(f, lt(3.15));
        EXPECT(f, le(3.15));
        EXPECT(f, gt(3.14));
        EXPECT(f, ge(3.14));
    }
}

出力

CUTEは実行されたテストケースを出力します。 また、テストケースやグループごとに実行時間の計測結果を出力するため、簡単なベンチマークとしても利用できます。

もしテストの実行中にエラーが発生した場合、その周囲のアサーションなどを出力します。 問題のない成功ケースの出力を省略することでエラー箇所の把握がしやすいようになっています。

仕組み

CUTEの大半の機能はマクロを用いて実装されています。

メインの機能であるアサーションは以下のようなコードに展開されます。

ASSERT(x, eq(42));
    ↓
CUTE_ASSERT(x, eq(42));
    ↓
CUTE_ASSERT_I(CUTE_TESTING, x, CUTE_PRED_eq(42));
    ↓
CUTE_ASSERT_II(cute_testing, x, CUTE_PRED_eq_COND, CUTE_PRED_eq_DESC, 42);
    ↓
CUTE_ASSERT_III(cute_testing, CUTE_PRED_eq_COND(x, 42), CUTE_PRED_eq_DESC(x, 42));
    ↓
CUTE_ASSERT_III(cute_testing, (x == 42), "x == 42"));
    ↓
do {
  if (!cute_assert(cute_testing, (x == 42), "x == 42", __FILE__, __LINE__)) {
    return;
  }
} while (0);

また、グルーピングは以下のように展開されます。

GROUP("group %d", 42) { ... }
    ↓
CUTE_GROUP("group %d", 42) { ... }
    ↓
CUTE_GROUP_I(CUTE_TESTING, "group %d", 42) { ... }
    ↓
CUTE_GROUP_I(cute_testing, "group %d", 42) { ... }
    ↓
for (
  cute_testing_t *_cute_tmp = cute_testing,
                 *cute_testing = CUTE_GROUP_START(_cute_tmp, "group %d", 42);
  cute_testing;
  CUTE_GROUP_FINISH(cute_testing), cute_testing = NULL
) { ... }

まとめ

ヘッダーオンリーのシンプルな単体テストフレームワークを作成しました。

個人のCプロジェクトの単体テストに用いてみたところひとまず問題なさそうなので満足しています。

*1:このあたりの命名GoogleTest に倣っています