CUnit のテストランナーを用意すればテストの実行がそれっぽくなるよという話

CUnit Home

CUnit にはテストランナーがありません。
テスト対象ごとに、main を作らないといけないのです。
JUnit に慣れた人からすると面倒ですし、同じような main をいくつも書くと DRY 原理主義者に恨まれてしまいます。

そこで、テストランナーだけでも作ってみたらどうなるか実験してみました。

方針

次のような構成にしてみました。
後で考えればやっぱりいけてないんですが、まあそれはそれで。




言葉で説明するとこんな感じです。

  • CUnit はテストスイート、テストグループを登録するレポジトリを持っています
    • スイートとグループは今のところ同じもののようです
  • CUnit の CU_basic_run_tests 関数は、レポジトリに登録されたテストスイートを実行します
  • 共有ライブラリには、テストスイート登録関数とテストケース関数が結合されています
  • テストスイート登録関数は、CUnit のレポジトリにテストスイートとテストケースを登録します
    • レポジトリへテストスイートを登録するには CU_add_suite 関数を使います
    • レポジトリへテストケースを登録するには CU_add_test 関数を使います
    • いずれにしても関数ポインタが登録されます
  • テストランナーは、共有ライブラリからテストスイート登録関数を動的に取得します
    • テストスイート登録関数の名前とテストコードの名前はそろえておきます
    • テストコードの名前はコマンドラインから指定します
  • テストランナーは、取得したテストスイート登録関数を実行します
  • テストランナーは、テストを実行し、結果を標準出力に出力します

コードの説明

テスティングフレームワーク側のコード

テストランナー (test_runner.c) はこんな感じです。

#include <stdio.h>
#include <stdlib.h>
#include <strings.h>

#include <dlfcn.h>

#include "my_cunit.h"

CU_pSuite pSuite;

int main(int argc, char* argv[])
{
        hLib = dlopen(getenv("CUNIT_LIBNAME"), RTLD_NOW|RTLD_GLOBAL|RTLD_WORLD);
        if (hLib == NULL)
        {
                printf("W,%s\n", dlerror());
                return -1;
        }

        CU_initialize_registry();

        // register each object function
        for (int i = 1; i < argc; ++i)
        {
                void (*suite_register_fn)(void) = (void (*)(void)) dlsym(hLib, argv[i]);
                if (suite_register_fn == NULL)
                {
                        printf("W,%s not found\n", argv[i]);
                }
                else
                {
                        printf("I,%s(%x) registered\n", argv[i], suite_register_fn);
                        (*suite_register_fn)();
                }
        }

        CU_basic_set_mode(CU_BRM_VERBOSE);
        CU_basic_run_tests();
        printf("\n");
        CU_basic_show_failures(CU_get_failure_list());
        printf("\n\n");
        CU_cleanup_registry();
        dlclose(hLib);
        return 0;
}

テスト用ヘッダ (my_cunit.h) です。

#ifndef MY_CUNIT_H
#define MY_CUNIT_H
#include "cunit/Headers/CUnit.h"
#include "cunit/Headers/Basic.h"
#include "cunit/Headers/TestDB.h"

extern CU_pSuite pSuite;
#define ADD_SUITE(desc, setup_fn, teardown_fn) pSuite = CU_add_suite(desc, setup_fn, teardown_fn);
#define ADD_TEST(desc, test_fn) CU_add_test(pSuite, desc, test_fn);

#endif
テスト対象側のコード

ヘッダ (square.h) です。

#ifndef SQUARE_H
#define SQUARE_H

int square(int n);

#endif

実装コード (square.c) です。

#include "square.h"

int square(int n) {
  return n * n;
}
テスト用のコード

テストコード (test_square.c) です。

#include "square.h"
#include "my_cunit.h"
#include <stdio.h>

void test_square_0_is_0() {
  CU_ASSERT_EQUAL(square(0), 0);
}

void test_square_4_is_16() {
  CU_ASSERT_EQUAL(square(4), 16);
}

void test_square_minus1_is_1() {
  CU_ASSERT_EQUAL(square(-1), 1);
}

void test_square_failure() {
  CU_FAIL("not implemented");
}

int setup() {
    /* nothing to do */
    puts("setup called");
    return 0;
}

int teardown() {
    /* nothing to do */
    puts("teardown called");
    return 0;
}

void test_square() {
  ADD_SUITE("square 関数のテスト", setup, teardown);
  ADD_TEST("0 の場合", test_square_0_is_0);
  ADD_TEST("4 の場合", test_square_4_is_16);
  ADD_TEST("-1 の場合", test_square_minus1_is_1);
  ADD_TEST("must be fail", test_square_failure);
}

実行するときの様子

実行する様子は次のようなかんじです。

テストランナーのコンパイル

一度だけビルドしてどこかに置いておけばいいでしょう。
LIBRARY_PATHlibcunit.a を置いておかないと駄目です。
cunit 関係のヘッダも見えるところに置いてください。

$ gcc -Wall -g -std=c99 -lcunit test_runner.c -o test_runner
test_runner.c: In function `main':
test_runner.c:32: warning: unsigned int format, pointer arg (arg 3)
テストコードのコンパイル

テストコードのコンパイル時もいろいろヘッダが見えてないといけません。

$ gcc -Wall -g -std=c99 -shared -fPIC square.c test_square.c -o testlib.so
テストの実行

やっとテストが実行できます。
test_runner.c にこっそり書いてますが、環境変数 CUNIT_LIBNAME にロードする対象のライブラリ名を仕込みます。

$ export CUNIT_LIBNAME=./testlib.so

それでは実行します。

$ ./test_runner test_square


     CUnit - A Unit testing framework for C - Version 2.1-0
     http://cunit.sourceforge.net/

setup called

Suite: square tests
  Test: arg is 0 ... passed
  Test: arg is 4 ... passed
  Test: arg is -1 ... passed
  Test: must be fail ... FAILED
    1. test_square.c:18  - CU_FAIL("not implemented")teardown called


--Run Summary: Type      Total     Ran  Passed  Failed
               suites        1       1     n/a       0
               tests         4       4       3       1
               asserts       4       4       3       1


  1. test_square.c:18  - CU_FAIL("not implemented")

結果を見てみましょう。

  • 成功したテストがちゃんとカウントされています (passed と出てますね)
  • 失敗すると思ってたテストがちゃんと失敗しています (FAILED と出てますね)
  • 失敗したテストの行番号とアサーションが出力されています
  • サマリーにテストの結果がほぼちゃんと出ています
    • suitesPassed がカウントされてません

考察と課題

実際のところ効率はどうなのか

今回はテストコードは 1 つでしたが、testlib.so にはまだテストコードをリンクする余地があります。
共有ライブラリのリンクと実行ファイルのリンクでどれくらい仕事量が違うかによりますが、都度実行ファイルを生成するよりは軽いんじゃないかと思ってます。
ただ、複数のオブジェクトファイルの中で関数名が重複すると困ったことになるので気を付けたほうがよいでしょう。
具体的には、setup/teardown の関数名が重ならないようにする必要があります。
gensym みたいなことができれば一番いいんでしょうね。

サマリでテストスーツの実行結果がおかしい

cunit/Basic.c で定義されている basic_all_tests_complete_message_handler を見ると分かりますが、なぜか "n/a" とハードコードされてます。
こんな感じで修正すればよいでしょう。

static void basic_all_tests_complete_message_handler(const CU_pFailureRecord pFailure)
{
  CU_pRunSummary pRunSummary = CU_get_run_summary();
  CU_pTestRegistry pRegistry = CU_get_registry();

  CU_UNREFERENCED_PARAMETER(pFailure); /* not used in basic interface */

  assert(NULL != pRunSummary);
  assert(NULL != pRegistry);

  if (CU_BRM_SILENT != f_run_mode)
    fprintf(stdout,"\n\n--Run Summary: Type      Total     Ran  Passed  Failed"
                     "\n               suites %8u%8u%8u%8u"
                     "\n               tests  %8u%8u%8u%8u"
                     "\n               asserts%8u%8u%8u%8u\n",
                    pRegistry->uiNumberOfSuites,
                    pRunSummary->nSuitesRun,
                    pRegistry->uiNumberOfSuites - pRunSummary->nSuitesFailed,
                    pRunSummary->nSuitesFailed,
                    pRegistry->uiNumberOfTests,
                    pRunSummary->nTestsRun,
                    pRunSummary->nTestsRun - pRunSummary->nTestsFailed,
                    pRunSummary->nTestsFailed,
                    pRunSummary->nAsserts,
                    pRunSummary->nAsserts,
                    pRunSummary->nAsserts - pRunSummary->nAssertsFailed,
                    pRunSummary->nAssertsFailed);
}

といったようにすぐになんとかなるくらい薄いフレームワークなので、自分で面倒見るつもりになってもいいと思います。