middle_unit
最小…よりも(ほんの)少し大きいテストフレームワーク

2021年2月27日

どもです。

今回は、単体テストのフレームワークについて書きます。

1.最小のフレームワーク

世の中に数多ある単体テストのフレームワークの中で、最も小さいモノは「min_unit」であると考えています。
(あくまで、私個人の意見/見解です。)
詳細は、コチラのサイトを参照してください。
このテストフレームワークは、たった1つのヘッダファイルのみから構成されています。
加えて、実装されているのも、「mu_assert()」と「mu_test_run()」の2つのマクロのみです。
これらの特徴から、このフレームワークはどのような環境(プラットフォーム)にも適用可能です。

私も、組み込み開発の単体テストでは、このフレームワークをよく使用します。

2.何が不満?

「min_unit」に対する不満は、下記が挙げられます。

  • 値の比較を行うコードを、自分で書かなければならない
    • 「=」と「==」を書き間違える可能性がある。
      (実際、仕事でやらかしたことがあります。
      このミス見つけたときは、シャレにならないくらい焦りました。)
  • 指定できるメッセージが、テストの出力と期待値が一致しない場合にしか表示されない。
    • テストの出力と期待値が一致した場合も、その旨知りたい!
    • テストの進捗が知りたい!
  • テストの出力と期待値が一致しないと、テストがその場で終了する。
    • 1つ直して再実行して、また別のバグを修正して再実行して…というやり方は効率が悪い。
    • 一通り最後までテストを実施して、NGとなるケースを洗い出したい!
  • メッセージを表示するのに、自分でprintf文を書かなければならない。
    • そこがイイトコロでもあるが、できれば書いてほしい!
  • 実行したテスト、成功したテスト、失敗したテストのそれぞれの件数を教えてほしい。
    • テストの件数の詳細(数)が知りたい!

3.不満を解消

3.1.min_unitの変更

というわけで、上記の不満を解消するべくmin_unitを変更したコードを、下記に示します。

#define mu_assert(message, expect, actual)                              \
    do {                                                                \
        if (!(expect == actual)) {                                      \
            printf("        = %s : NG\r\n", message);                   \
            printf("            - expected : %d\r\n", (int)expect);     \
            printf("            - actual   : %d\r\n", (int)actual);     \
            return message;                                             \
        } else {                                                        \
            printf("        = %s : OK\r\n", message);                   \
        }                                                               \
    } while (0)

#define mu_run_test(test_name, test)                                    \
    do {                                                                \
        printf("    [%s : START]\r\n", test_name);                      \
        char *message = test();                                         \
        tests_run++;                                                    \
        if (message) {                                                  \
            printf("    [%s : FAILED]\r\n", test_name);                 \
            tests_failed++;                                             \
        } else {                                                        \
            printf("    [%s : PASSED]\r\n", test_name);                 \
            tests_passed++;                                             \
        }                                                               \
    } while (0)

#define mu_run_all_test(test_name, test)                                \
    do {                                                                \
        printf("{%s : START}\r\n", test_name);                          \
        tests_run = 0;                                                  \
        tests_passed = 0;                                               \
        tests_failed = 0;                                               \
        test();                                                         \
        if (0 < tests_failed) {                                         \
            printf("{%s : FAILED}\r\n", test_name);                     \
        } else {                                                        \
            printf("{%s : ALL PASSED}\r\n", test_name);                 \
        }                                                               \
        printf("    [%d tests exeucted.]\n", tests_run);                \
        printf("    [%d tests passed.]\n", tests_passed);               \
        printf("    [%d tests failed.]\n", tests_failed);               \
    } while (0)

extern int tests_run;
extern int tests_passed;
extern int tests_failed;

3.2.min_unitからの変更点

min_unitから、以下のような変更を実施しています。

  • mu_assertの引数を増やして、テストの出力と期待値を渡すように変更
  • テストの出力と期待値が一致しない場合、それぞれの値を表示する。
  • テストの出力と期待値が一致した場合でも、メッセージを表示する。
  • mu_run_testの先頭で、テスト開始を示すメッセージを表示する。
  • mu_run_testの完了時に、成功/失敗を示すメッセージを同時に表示する。
  • mu_run_all_test()を新規に追加

一番大きな変更は、mu_assert()マクロのI/Fの変更かと思います。
マクロのI/Fが一部変更になっているので、minunitとは互換性がなくなってしまっています。
それでも、minunit側を少し変更するだけで十分に対応可能な範囲かと思います。
新規に追加した「mu_run_all_test()」マクロについては、実は使用しなくてもテストは実行可能です。
そのため、minunitとの互換性には影響はありません。

4.使ってみました

以下のような簡単な関数の単体テストを、改変/作成したフレームワークを使用して行ってみます。

int add(int val1, int val2, int* result)
{
    int res = 0;
    if (NULL == result) {
        return 0;
    } else {
        *result = val1 + val2;
        if (*result < 0) {
            res = (-1);
        } else if (0 <= *result) {
            res = 1;
        }
    }
    return res;
}

まず、テストケースを実行する処理の実装です。
実装は、下記のようになります。

int add(int val1, int val2, int* result);
        
static char* target_test_add_001()
{
    int val1 = 0;
    int val2 = 0;
    int result = 0;
    int res = 0;

    res = add(val1, val2, &result);

    mu_assert("result", result, 0);
    mu_assert("res", res, 1);

    return 0;
}

static char* target_test_add_002()
{
    int val1 = -1;
    int val2 = 0;
    int result = 0;
    int res = 0;

    res = add(val1, val2, &result);

    mu_assert("result", result, (-1));
    mu_assert("res", res, (-1));

    return 0;
}

static char* target_test_add_003()
{
    int val1 = -1;
    int val2 = 1;
    int result = 0;
    int res = 0;

    res = add(val1, val2, (int*)&result);

    mu_assert("result", result, 0);
    mu_assert("res", res, 1);

    return 0;
}

static char* target_test_add_004()
{
    int val1 = -1;
    int val2 = 0;
    int result = 0;
    int res = 0;

    res = add(val1, val2, (int*)0);

    mu_assert("res", res, 0);

    return 0;
}

char* run_target_test_add()
{
    mu_run_test("target_test_add_001", target_test_add_001);
    mu_run_test("target_test_add_002", target_test_add_002);
    mu_run_test("target_test_add_003", target_test_add_003);
    mu_run_test("target_test_add_004", target_test_add_004);

    return 0;
}

前述の通り、mu_assert()の引数を増やし、かつ第1引数には「結果がNGの場合に表示したいメッセージ」ではなく、「確認している内容を示す文字列」を設定しています。
また、main()関数の実装は、下記の通りです。

#include <stdio.h>
#include "../src/mid_unit.h"

int tests_run = 0;
int tests_passed = 0;
int tests_failed = 0;

char* run_target_test_add();

int main()
{
    mu_run_all_test("run_target_test_add", run_target_test_add);

    return 0;
}

先頭の3つの変数は、どうしてもここで宣言しなければならない、フレームワーク側で使用する変数の宣言です。
min_unitを使用した場合でも、同様の宣言が必要です。
これらのコードを実装したら、以下のコマンドを実行してビルドとテストの実行を行います。

gcc *.c -o example.exe ; ./example.exe

ビルドとテストの実行は、cygwin上で行っています。
テストを実行すると、コンソールには下図のような表示になります。
test_framework_mid_unit_001_001
うん。
イイ感じです。

5.まとめ

今回、テストフレームワークである「min_unit」を、少しだけですが改変してみました。
結果として、これまで「かゆいところに手が届かない」と思っていたことが解消できました。
ほんの少しでも良いので、単体テストをやりやすくすることに貢献できていれば幸いです。

ではっ!

ex.公開しています

今回作成したフレームワークですが、エントリのタイトルにもあるように「middle_unit」と名付けました。
GitHubにて公開しています。
参考にしていただけたら、嬉しいです。
「いいね!」していただけたら、もっと嬉しいです。