この記事はあくあたん工房 クリスマスアドベントカレンダー2020の2日目の記事として書かれたものです。
はじめに
この記事では、C言語向けテストフレームワークであるCUnitによるテストを行ってみます。
CUnitは導入が簡単で、単純な単体テストも、とても簡単に始めることが出来ます。
しかし、少し踏み込んだ使い方をしてみたいと思った際に少し困ってしまう部分がありました。
この記事では、そういった場面で上手く使うための方法を紹介しつつ、
ワークスペースの作成からGitHub Actionsでテストを走らせる部分までやってみたいと思います。
環境
PC     | ASUS ZenBook UX303LN
OS     | Arch Linux x86_64
Kernel | 5.9.9-arch1-1
gcc    | 10.2.0
CUnit  | 2.1.3
利用したソースコードは、GitHubにまとめてあるので、活用してください。
CUnitのインストール
Arch Linuxならpacman、Ubuntuならaptを使ってインストールできます。
$ sudo pacman -S cunit
$ sudo apt install libcunit1-dev
テストしたいプログラムを作成する
fizzbuzzプログラムを書く
ここからは、「CUnit使ってみた」を参考に進めてみます。
テストに使うFizzBuzzのソースコードもここからお借りしました。
今回は、1〜19までの数字に対してFizzBuzzを答えるプログラムを作成します。
ここで、作成したプログラムは「Initial Commit - GitHub」にあります。
#include <stdio.h>
#include "fizzbuzz.h"
int main(void) {
    int i;
    char result[256];
    for (i = 1; i < 20; i++) {
        fizzbuzz(i, result);
        printf("%s\n", result);
    }
    return 0;
}
#ifndef _FIZZBUZZ_H_
#define _FIZZBUZZ_H_
void fizzbuzz(int, char *);
#endif /* _FIZZBUZZ_H_ */
#include <stdio.h>
void fizzbuzz(int num, char *result) {
    if ((num % 3 == 0) && (num % 5 == 0)) {
        sprintf(result, "FizzBuzz");
    } else if (num % 3 == 0) {
        sprintf(result, "Fizz");
    } else if (num % 5 == 0) {
        sprintf(result, "Buzz");
    } else {
        sprintf(result, "%d", num);
    }
}
fizzbuzzプログラムをコンパイルするMakefileを書く
コンパイルは以下のコマンドで出来ますが、後でCUnitを使うためにMakefileを書くのでここで作成しておきます。
$ gcc main.c fizzbuzz.c -o main
CC := gcc
OBJS := main.o fizzbuzz.o
CFLAGS := -W -Wall
main: $(OBJS)
$(OBJS): fizzbuzz.h 
.PHONY: clean
clean:
	-rm *.o 
	-rm main
.DEFAULT_GOAL=main
CUnitを使ってFizzBuzzをテストするプログラムを作成する
テストするプログラムを書く
このFizzBuzzの出力をテストするプログラム(test.c)を書いてみます。
まずはmain関数ですが、流れは以下のようになっています。
- テストレジストリの初期化 - CU_initialize_registry()
- テストスイートをテストレジストリに追加 - CU_add_suite()
- テストケースをテストスイートに追加 - CU_add_test()
- テストを実行 - CU_console_run_tests()
- テストレジストリを削除 - CU_cleanup_registry()
#include <CUnit/CUnit.h>
#include <CUnit/Console.h>
#include <CUnit/TestDB.h>
void fizzbuzz_test_1(void);
int main(void) {
    CU_pSuite suite;
    CU_initialize_registry();
    suite = CU_add_suite("FizzBuzz Test", NULL, NULL);
    CU_add_test(suite, "FizzBuzz_Test_1", fizzbuzz_test_1);
    CU_console_run_tests();
    CU_cleanup_registry();
    return 0;
}
ここでは、"FizzBuzz Test"という名前でテストスイートを用意し、fizzbuzz_test_1(void)という関数をFizzBuzz_Test_1という名前でテストケースに登録しています。
また、CUnitにはいくつかモードがあります。今回はコンソールでの対話でテストを行うのでCUnit/Console.hをincludeしてCU_console_run_tests()を実行していますが、モードによって関数名が異なるので注意です。
次に、この関数に、fizzbuzzの1に対する解答が正しいかをテストする部分を書いてみます。
#include <string.h>
#include "fizzbuzz.h"
int main(void){
...
}
void fizzbuzz_test_1(void) {
    char result[256];
    memset(result, '\0', sizeof(result));
    fizzbuzz(1, result);
    CU_ASSERT_STRING_EQUAL("1", result);
    return;
}
fizzbuzz_test_1()関数を作成しました。
CU_ASSERT_STRING_EQUAL()で、resultの文字列と"1"がイコールかを判定できます。
なお、他のAssertは「2.2. CUnit Assertions」に書かれているので参考にしてください。
似たようにして、3でFizz、5でBuzz、15でFizzBuzzを出力できるかテストする関数を作成してテストケースとして追加しました。
ソースコードは「test用ソースファイルを追加 - GitHub」からでも見れます。
ソースコード折りたたみ
#include <CUnit/CUnit.h>
#include <CUnit/Console.h>
#include <CUnit/TestDB.h>
#include <string.h>
#include "fizzbuzz.h"
void fizzbuzz_test_1(void);
void fizzbuzz_test_3(void);
void fizzbuzz_test_5(void);
void fizzbuzz_test_15(void);
int main(void) {
    CU_pSuite suite;
    CU_initialize_registry();
    suite = CU_add_suite("FizzBuzz Test", NULL, NULL);
    CU_add_test(suite, "FizzBuzz_Test_1", fizzbuzz_test_1);
    CU_add_test(suite, "FizzBuzz_Test_3", fizzbuzz_test_3);
    CU_add_test(suite, "FizzBuzz_Test_5", fizzbuzz_test_5);
    CU_add_test(suite, "FizzBuzz_Test_15", fizzbuzz_test_15);
    CU_console_run_tests();
    CU_cleanup_registry();
    return 0;
}
void fizzbuzz_test_1(void) {
    char result[256];
    memset(result, '\0', sizeof(result));
    fizzbuzz(1, result);
    CU_ASSERT_STRING_EQUAL("1", result);
    return;
}
void fizzbuzz_test_3(void) {
    char result[256];
    memset(result, '\0', sizeof(result));
    fizzbuzz(3, result);
    CU_ASSERT_STRING_EQUAL("Fizz", result);
    return;
}
void fizzbuzz_test_5(void) {
    char result[256];
    memset(result, '\0', sizeof(result));
    fizzbuzz(5, result);
    CU_ASSERT_STRING_EQUAL("Buzz", result);
    return;
}
void fizzbuzz_test_15(void) {
    char result[256];
    memset(result, '\0', sizeof(result));
    fizzbuzz(15, result);
    CU_ASSERT_STRING_EQUAL("FizzBuzz", result);
    return;
}
テストプログラムをコンパイルするMakefileを書く
このtest.cをコンパイルするには、CUnitのライブラリをリンクする必要があるので、このようになります。
$ gcc test.c fizzbuzz.c -W -Wall -L/usr/lib  -lcunit -o test
これを手打ちするのは面倒なので、Makefileを修正しておきます。
これは「test用ソースファイルを追加 - GitHub」からdiffが見れます。
CC := gcc
OBJS := main.o fizzbuzz.o
CFLAGS := -W -Wall
TEST_CFLAGS := $(CFLAGS)
TEST_LIBDIR := -L/usr/lib
TEST_LIB := -lcunit
all: main test
main: $(OBJS)
test: test.c fizzbuzz.c
	$(CC) $^ $(TEST_CFLAGS) $(TEST_LIBDIR) $(TEST_LIB) -o $@
$(OBJS): fizzbuzz.h 
.PHONY: clean
clean:
	-rm *.o 
	-rm main test
.DEFAULT_GOAL=all
テストを実行する
実際にコンパイルして実行してみます。
CUnitのコンソールが表示されたらRを入力するとテストを実行できます。
$ make test
gcc test.c fizzbuzz.c -W -Wall -L/usr/lib -lcunit -o test
$ ./test
     CUnit - A Unit testing framework for C - Version 2.1-3
             http://cunit.sourceforge.net/
***************** CUNIT CONSOLE - MAIN MENU ******************************
(R)un  (S)elect  (L)ist  (A)ctivate  (F)ailures  (O)ptions  (H)elp  (Q)uit
Enter command: R
Running Suite : FizzBuzz Test
     Running Test : FizzBuzz_Test_1
     Running Test : FizzBuzz_Test_3
     Running Test : FizzBuzz_Test_5
     Running Test : FizzBuzz_Test_15
Run Summary:    Type  Total    Ran Passed Failed Inactive
              suites      1      1    n/a      0        0
               tests      4      4      4      0        0
             asserts      4      4      4      0      n/a
Elapsed time =    0.000 seconds
Failedは0個で、全てのテストをパス出来ました!
テクニックの紹介
ここからは、少し特殊なケースを想定してテストを行う方法を考えてみたいと思います。
ここの内容は、「Unit-testing static functions in C」を参考にしています。
fizzbuzz.cに書いてあるstaticな関数に対してテストをしたい
fizzbuzzプログラムに機能を追加した
fizzbuzz.c を少し修正してみました。「fizzbuzzに機能を追加した - GitHub」
FizzBuzzの文字列は変数として用意されていて、それを書き換えることでFizzBuzz以外の文字列にも対応できる、というものです。
#include <stdio.h>
static const char *get_str_FizzBuzz(void);
static const char *get_str_Fizz(void);
static const char *get_str_Buzz(void);
static const char *STR_FIZZBUZZ = "FizzBuzz";
static const char *STR_FIZZ = "Fizz";
static const char *STR_BUZZ = "Buzz";
void fizzbuzz(int num, char *result) {
    if ((num % 3 == 0) && (num % 5 == 0)) {
        sprintf(result, get_str_FizzBuzz());
    } else if (num % 3 == 0) {
        sprintf(result, get_str_Fizz());
    } else if (num % 5 == 0) {
        sprintf(result, get_str_Buzz());
    } else {
        sprintf(result, "%d", num);
    }
}
static const char *get_str_FizzBuzz(void) {
    return STR_FIZZBUZZ;
}
static const char *get_str_Fizz(void) {
    return STR_FIZZ;
}
static const char *get_str_Buzz(void) {
    return STR_BUZZ;
}
テストプログラムを修正する
このように、外からアクセスされないようなstaticな関数を定義していることがあるはずです。
これらの関数は、test.cからアクセスできないので、テストを行うことが出来ません。
test.cに関数をコピペしても出来ることには出来ますが、fizzbuzz.cで修正したものをそのまま使えれば嬉しいです。
なので、こうしてみます。
  #include <CUnit/CUnit.h>
  #include <CUnit/Console.h>
  #include <CUnit/TestDB.h>
+ #include "fizzbuzz.c"
  #include "fizzbuzz.h"
test.cでfizzbuzz.cをincludeします。
これでtest.c内からfizzbuzz.c内にあるstaticな関数にアクセスできます。
ということで、テストを用意してみました(テストの内容はしょぼい…。)。
void get_str_test(void);
int main(void) {
    CU_pSuite suite;
    CU_initialize_registry();
    suite = CU_add_suite("FizzBuzz Test", NULL, NULL);
    CU_add_test(suite, "FizzBuzz_Test_1", fizzbuzz_test_1);
    CU_add_test(suite, "FizzBuzz_Test_3", fizzbuzz_test_3);
    CU_add_test(suite, "FizzBuzz_Test_5", fizzbuzz_test_5);
    CU_add_test(suite, "FizzBuzz_Test_15", fizzbuzz_test_15);
    // テストを追加
    suite = CU_add_suite("FizzBuzz String Test", NULL, NULL);
    CU_add_test(suite, "get_str_test", get_str_test);
    CU_console_run_tests();
    CU_cleanup_registry();
    return 0;
}
void get_str_test(void) {
    CU_ASSERT_STRING_EQUAL("Fizz", get_str_Fizz());
    CU_ASSERT_STRING_EQUAL("Buzz", get_str_Buzz());
    CU_ASSERT_STRING_EQUAL("FizzBuzz", get_str_FizzBuzz());
    return;
}
Makefileを修正する
この修正に合わせて、Makefileも書きかえます。
 CC := gcc
+SRCS := fizzbuzz.c
 OBJS := main.o fizzbuzz.o
 CFLAGS := -W -Wall
-test: test.c fizzbuzz.c
-       $(CC) $^ $(TEST_CFLAGS) $(TEST_LIBDIR) $(TEST_LIB) -o $@
+test: test.c $(SRCS)
+       $(CC) $< $(TEST_CFLAGS) $(TEST_LIBDIR) $(TEST_LIB) -o $@
 $(OBJS): fizzbuzz.h
ここまでのコードは「staticな関数に対してテスト出来るようにした - GitHub」にあります。
テストを実行する
さて、これをコンパイルして、実行してみると、テストをパスしているのがわかりますね。
Running Suite : FizzBuzz Test
     Running Test : FizzBuzz_Test_1
     Running Test : FizzBuzz_Test_3
     Running Test : FizzBuzz_Test_5
     Running Test : FizzBuzz_Test_15
Running Suite : FizzBuzz String Test
     Running Test : get_str_test
Run Summary:    Type  Total    Ran Passed Failed Inactive
              suites      2      2    n/a      0        0
               tests      5      5      5      0        0
             asserts      7      7      7      0      n/a
Elapsed time =    0.000 seconds
main.cにある関数を呼び出して使いたい
fizzbuzzプログラムに機能を追加した
例えば、main.cにエラー時のメッセージを表示する関数があったとします。
int error(char *mes) {
    fprintf(stderr, "ERROR:%s\n", mes);
    return -1;
}
これをfizzbuzz()がエラー時に使用する、という状況を考えます。
今回は、20以上の数字に対してfizzbuzzを答えようとするとエラーを表示する、というものにしています。
また、この時、fizzbuzz()は-1を返すようにしてプログラムを終了させています。
 #ifndef _FIZZBUZZ_H_
 #define _FIZZBUZZ_H_
-void fizzbuzz(int, char *);
+int fizzbuzz(int, char *);
+int error(char *);
 #endif /* _FIZZBUZZ_H_ */
 int main(void) {
     int i;
     char result[256];
     for (i = 1; i < 20; i++) {
-        fizzbuzz(i, result);
+        if (fizzbuzz(i, result) == -1) {
+            exit(EXIT_FAILURE);
+        }
         printf("%s\n", result);
     }
     return 0;
 }
+#include "fizzbuzz.h"
+
 #include <stdio.h>
-
-void fizzbuzz(int num, char *result) {
+int fizzbuzz(int num, char *result) {
+    if (20 <= num) {
+        return error("20以上の値が指定されました");
+    }
+
     if ((num % 3 == 0) && (num % 5 == 0)) {
         sprintf(result, get_str_FizzBuzz());
     } else if (num % 3 == 0) {
 ...
     } else {
         sprintf(result, "%d", num);
     }
+
+    return 0;
 }
ヘッダーファイルにerror()関数のプロトタイプ宣言を記述し、fizzbuzz.cからでもアクセス出来るようにしました。
このコードは「fizzbuzzでエラー出力を行うようにした - GitHub」にあります。
テストを書くときに発生する問題点
これでプログラム自体は動作するのですが、テストを書くときに問題が発生します。
テスト用の実行ファイルを生成するためには、main.cをコンパイル時に含める必要があるので、コンパイルコマンドはこのようになります。
$ gcc test.c main.c -W -Wall -L/usr/lib -lcunit -o test
/usr/bin/ld: /tmp/ccUX9o67.o: in function `main':
main.c:(.text+0x0): multiple definition of `main'; /tmp/ccnKWgCa.o:test.c:(.text+0x16a): first defined here
collect2: エラー: ld はステータス 1 で終了しました
が、これはコンパイルできません。
test.cとmain.c内のどちらにも、main関数が記述されてしまっているからです。
解決策
そこで、マクロを使ってmain.cにあるmain関数の名前を別名に変更することで対応してみます。
つまり、このようなマクロを定義しておいて、mainを_main_disabled_に変更するということです。
#define main _main_disabled_
このマクロをtest.cに記述してプリプロセッサ出力を見てみましょう。
-Eオプションをつけることで、プリプロセッサ出力を表示できます。
$ gcc -E test.c
...
void fizzbuzz_test_30(void);
void get_str_test(void);
int _main_disabled_(void) {
    CU_pSuite suite;
    CU_initialize_registry();
...
確かに変わっていますが、test.cのmain関数の名前が変わっていますね…。
main.cのmain関数の名前だけが変わるようにしたいです。
そのため、先程と同様にmain.cをincludeし、test.cのmain関数には適用されないよう、直前で#undef mainを使って、mainマクロの定義を無効にすることで対応します。
また、このmainマクロはコンパイラ引数で与える方が良さそうです。
テストプログラムとMakefileに解決策を施す
以上のことを踏まえて、次のように修正します。
 #include "fizzbuzz.c"
 #include "fizzbuzz.h"
+#include "main.c"
 void fizzbuzz_test_1(void);
 void fizzbuzz_test_3(void);
 void fizzbuzz_test_15(void);
 void get_str_test(void);
+#undef main
 int main(void) {
     CU_pSuite suite;
     CU_initialize_registry();
 }
 OBJS := main.o fizzbuzz.o
 CFLAGS := -W -Wall
-TEST_CFLAGS := $(CFLAGS)
+TEST_CFLAGS := $(CFLAGS) -Dmain=_main_disabled
 TEST_LIBDIR := -L/usr/lib
 TEST_LIB := -lcunit
オプションでマクロを定義するには-Dオプションを使用します。
このようにすることで、defineマクロを定義したのと同様の動作をするようになります。
これで、#include "main.c"内のmain関数名にはmainマクロが適用されるため、名前が置き換えられますが、test.cのmain関数名は直前で#undef mainされているので、置き換わらないように出来ました。
なので、コンパイルが通ります。
ここでのコードは「mainが重複する問題を解決した - GitHub」にあります。
テストプログラムにテストケースを追加する
これで、test.cからerror()関数を呼び出せるようになったので、テストを書いてみましょう。
20以上の値を与えればよいので、今回は30をfizzbuzz()に与えてみます。
CU_ASSERT_NOT_EQUAL_FATALはfizzbuzz(30, result)の返り値と0が一致しなければOK、というものです。
fizzbuzz(30, result)の返り値は-1なので、一致しないはずです。
+void fizzbuzz_test_30(void);
 void get_str_test(void);
 #undef main
 int main(void) {
     CU_add_test(suite, "FizzBuzz_Test_3", fizzbuzz_test_3);
     CU_add_test(suite, "FizzBuzz_Test_5", fizzbuzz_test_5);
     CU_add_test(suite, "FizzBuzz_Test_15", fizzbuzz_test_15);
+    CU_add_test(suite, "FizzBuzz_Test_30", fizzbuzz_test_30);
     suite = CU_add_suite("FizzBuzz String Test", NULL, NULL);
     CU_add_test(suite, "get_str_test", get_str_test);
     return;
 }
+
+void fizzbuzz_test_30(void) {
+    char result[256];
+    memset(result, '\0', sizeof(result));
+
+    CU_ASSERT_NOT_EQUAL_FATAL(fizzbuzz(30, result), 0);
+    return;
+}
+
このコードは「error関数のテストを書いた - GitHub」にあります。
テストを実行する
このテストを実行してみましたが、無事パス出来ていることが確認できました。
Running Suite : FizzBuzz Test
     Running Test : FizzBuzz_Test_1
     Running Test : FizzBuzz_Test_3
     Running Test : FizzBuzz_Test_5
     Running Test : FizzBuzz_Test_15
     Running Test : FizzBuzz_Test_30ERROR:20以上の値が指定されました
Running Suite : FizzBuzz String Test
     Running Test : get_str_test
Run Summary:    Type  Total    Ran Passed Failed Inactive
              suites      2      2    n/a      0        0
               tests      6      6      6      0        0
             asserts      8      8      8      0      n/a
Elapsed time =    0.000 seconds
テストのカバレッジを計測する
gccではコードのカバレッジ計測にgcovというツールを使うことができます。
gcovはカバレッジ計測以外にも、プロファイリングツールとしても使えます。
使い方は、gcovマニュアルを参考にします。
カバレッジの計測を有効にする
カバレッジの計測を有効にするには、コンパイルオプションを追加するだけです。
 OBJS := main.o fizzbuzz.o
 CFLAGS := -W -Wall
-TEST_CFLAGS := $(CFLAGS) -Dmain=_main_disabled
+TEST_CFLAGS := $(CFLAGS) -Dmain=_main_disabled -fprofile-arcs -ftest-coverage
 TEST_LIBDIR := -L/usr/lib
 TEST_LIB := -lcunit
 clean:
        -rm *.o
        -rm main test
+       -rm *.gcno *.gcov *.gcda *.gch
このコードは「カバレッジ計測を行うようにした - GitHub」にあります。
カバレッジの計測(命令網羅)を行い、結果を確認する
カバレッジの計測は、コンパイルして生成された実行ファイルを実行するたびに行われます。
なので、
- 
make testで実行ファイルを生成する
- 
./testでCUnitによるテストを起動し、Rで実行する
- 結果を見る
という流れになります。手順2.を終えた時点で、test.gcda、test.gcnoのようなファイルが生成されているかと思います。
計測結果は実行するたびに上書きされるのではなく、各行の実行回数などは蓄積されていきます。
また、中身はバイナリファイルなのでエディタなどで開くことは出来ません。
$ file test.gcda
test.gcda: GCC gcda coverage (-fprofile-arcs), version B.2
これを元に、gcovで情報を出力させます。
$ gcov test.gcda -l
File 'test.c'
実行された行:100.00% of 42
Creating 'test.gcda##test.c.gcov'
File 'main.c'
実行された行:33.33% of 9
Creating 'test.gcda##main.c.gcov'
File 'fizzbuzz.c'
実行された行:100.00% of 17
Creating 'test.gcda##fizzbuzz.c.gcov'
includeされたソースコードと、includeした場所を対応づけて生成されるように-lオプションを指定しました。
このように、CUnitでテストを行う際に、ソースコードをincludeする方法を用いましたが、適切にファイルごとの計測結果を出力してくれるのは嬉しいポイントですね。
ここで表示されるのは、命令網羅(ステートメントカバレッジ、C0)の測定結果です。
同時に、新たに以下のファイルが生成され、各ファイルごとの詳細を見ることが出来ます。
- test.gcda##fizzbuzz.c.gcov
- test.gcda##main.c.gcov
- test.gcda##test.c.gcov
##より右側についている名前とソースコードのファイル名が対応しており、中身は単なるテキストファイルなので好きなエディタで開くことができます。
fizzbuzz.cに対する結果を見てみると、最も左側に書かれている数字が実行された回数なので、どの行も少なくとも1回は実行されていることがわかります。
main.cに対する結果ではmain関数の実行回数が#####になっていて、一度も実行されていないことがわかります。
結果は「カバレッジ計測(命令網羅)をした - GitHub」でも見れます。
`fizzbuzz.c`のカバレッジ計測結果(命令網羅)折りたたみ
        -:    0:Source:fizzbuzz.c
        -:    0:Graph:test.gcno
        -:    0:Data:test.gcda
        -:    0:Runs:1
        -:    1:#include "fizzbuzz.h"
        -:    2:
        -:    3:#include <stdio.h>
        -:    4:
        -:    5:static const char *get_str_FizzBuzz(void);
        -:    6:static const char *get_str_Fizz(void);
        -:    7:static const char *get_str_Buzz(void);
        -:    8:
        -:    9:static const char *STR_FIZZBUZZ = "FizzBuzz";
        -:   10:static const char *STR_FIZZ = "Fizz";
        -:   11:static const char *STR_BUZZ = "Buzz";
        -:   12:
        5:   13:int fizzbuzz(int num, char *result) {
        5:   14:    if (20 <= num) {
        1:   15:        return error("20以上の値が指定されました");
        -:   16:    }
        -:   17:
        4:   18:    if ((num % 3 == 0) && (num % 5 == 0)) {
        1:   19:        sprintf(result, get_str_FizzBuzz());
        3:   20:    } else if (num % 3 == 0) {
        1:   21:        sprintf(result, get_str_Fizz());
        2:   22:    } else if (num % 5 == 0) {
        1:   23:        sprintf(result, get_str_Buzz());
        -:   24:    } else {
        1:   25:        sprintf(result, "%d", num);
        -:   26:    }
        -:   27:
        4:   28:    return 0;
        -:   29:}
        -:   30:
        2:   31:static const char *get_str_FizzBuzz(void) {
        2:   32:    return STR_FIZZBUZZ;
        -:   33:}
        -:   34:
        2:   35:static const char *get_str_Fizz(void) {
        2:   36:    return STR_FIZZ;
        -:   37:}
        -:   38:
        2:   39:static const char *get_str_Buzz(void) {
        2:   40:    return STR_BUZZ;
        -:   41:}
カバレッジの計測(条件網羅)を行い、結果を確認する
また、条件網羅(ブランチカバレッジ、C1)の計測結果も調べることができ、-bオプションをつけるだけです。
$ gcov test.gcda -l -b
File 'test.c'
実行された行:100.00% of 42
分岐がありません
実行された呼び出し:100.00% of 27
Creating 'test.gcda##test.c.gcov'
File 'main.c'
実行された行:33.33% of 9
実行された分岐:0.00% of 4
Taken at least once:0.00% of 4
実行された呼び出し:25.00% of 4
Creating 'test.gcda##main.c.gcov'
File 'fizzbuzz.c'
実行された行:100.00% of 17
実行された分岐:100.00% of 10
Taken at least once:100.00% of 10
実行された呼び出し:100.00% of 4
Creating 'test.gcda##fizzbuzz.c.gcov'
結果は「カバレッジ計測(条件網羅)をした - GitHub」でも見れます。
`fizzbuzz.c`のカバレッジ計測結果(条件網羅)折りたたみ
        -:    0:Source:fizzbuzz.c
        -:    0:Graph:test.gcno
        -:    0:Data:test.gcda
        -:    0:Runs:1
        -:    1:#include "fizzbuzz.h"
        -:    2:
        -:    3:#include <stdio.h>
        -:    4:
        -:    5:static const char *get_str_FizzBuzz(void);
        -:    6:static const char *get_str_Fizz(void);
        -:    7:static const char *get_str_Buzz(void);
        -:    8:
        -:    9:static const char *STR_FIZZBUZZ = "FizzBuzz";
        -:   10:static const char *STR_FIZZ = "Fizz";
        -:   11:static const char *STR_BUZZ = "Buzz";
        -:   12:
function fizzbuzz called 5 returned 100% blocks executed 100%
        5:   13:int fizzbuzz(int num, char *result) {
        5:   14:    if (20 <= num) {
branch  0 taken 20% (fallthrough)
branch  1 taken 80%
        1:   15:        return error("20以上の値が指定されました");
call    0 returned 100%
        -:   16:    }
        -:   17:
        4:   18:    if ((num % 3 == 0) && (num % 5 == 0)) {
branch  0 taken 50% (fallthrough)
branch  1 taken 50%
branch  2 taken 50% (fallthrough)
branch  3 taken 50%
        1:   19:        sprintf(result, get_str_FizzBuzz());
call    0 returned 100%
        3:   20:    } else if (num % 3 == 0) {
branch  0 taken 33% (fallthrough)
branch  1 taken 67%
        1:   21:        sprintf(result, get_str_Fizz());
call    0 returned 100%
        2:   22:    } else if (num % 5 == 0) {
branch  0 taken 50% (fallthrough)
branch  1 taken 50%
        1:   23:        sprintf(result, get_str_Buzz());
call    0 returned 100%
        -:   24:    } else {
        1:   25:        sprintf(result, "%d", num);
        -:   26:    }
        -:   27:
        4:   28:    return 0;
        -:   29:}
        -:   30:
function get_str_FizzBuzz called 2 returned 100% blocks executed 100%
        2:   31:static const char *get_str_FizzBuzz(void) {
        2:   32:    return STR_FIZZBUZZ;
        -:   33:}
        -:   34:
function get_str_Fizz called 2 returned 100% blocks executed 100%
        2:   35:static const char *get_str_Fizz(void) {
        2:   36:    return STR_FIZZ;
        -:   37:}
        -:   38:
function get_str_Buzz called 2 returned 100% blocks executed 100%
        2:   39:static const char *get_str_Buzz(void) {
        2:   40:    return STR_BUZZ;
        -:   41:}
GitHub ActionsでCUnitを走らせる
最後に、GitHub ActionsでCUnitによるテストを行うようにしてみます。
GitHub Actions用のyamlファイルを書く
GitHub Actions用のyamlファイルを.github/workflows/に配置します。
$ mkdir -p .github/workflows
$ cd .github/workflows
$ touch actions.yml
今回は以下のように設定しました。
- masterまたはmainにpushした際に実行
- Ubuntu-latestの環境を使用
jobの流れ
- CUnitをUbuntuにインストール
- 
make testで実行ファイルを生成
- 
make checkでテストを実行
name: CI
on:
  push:
    branches: [ master, main ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Install CUnit
        run: |
            sudo apt update
            sudo apt install -y libcunit1-dev
      - name: CUnit Test
        run: |
            make test
            make check
Makefileを修正する
make checkは、単に生成した実行ファイルを実行するだけです。
これに合わせて、Makefileに追記しておきます。
+.PHONY: check
+check: test
+       @./test
+
 .PHONY: clean
 clean:
        -rm *.o
テストプログラムを修正する
GitHub Actionsでテストを行うとき、コンソールによる対話は使えません。
なので、Basicモードを利用します。
ヘッダファイルをincludeして、Basicモード用の関数を呼び出すだけです。
+#include <CUnit/Basic.h>
 #include <CUnit/CUnit.h>
 #include <CUnit/Console.h>
 #include <CUnit/TestDB.h>
 int main(void) {
     suite = CU_add_suite("FizzBuzz String Test", NULL, NULL);
     CU_add_test(suite, "get_str_test", get_str_test);
-
-    CU_console_run_tests();
+    // CU_console_run_tests();
+    CU_basic_run_tests();
     CU_cleanup_registry();
     return 0;
 }
また、この実行ファイルは常に終了コードとして0を返すため、GitHub Actionsは正常に終了したとみなされます。
そのため、テストに通らなかったテストケースが存在していた場合も正常に終了してしまいます。
そこで、プログラムの終了コードを「テストに通らなかったテストケースの数」として、0以外の値を返すようにしておきます。
 int main(void) {
+    int ret;
     CU_pSuite suite;
     CU_initialize_registry();
     CU_basic_run_tests();
+    ret = CU_get_number_of_failures();
     CU_cleanup_registry();
-    return 0;
+    return ret;
 }
ここまでのコードは「GitHub ActionsでCUnitを走らせるようにした - GitHub」にあります。
GitHub Actionsでテストを実行する
これをGitHubのリポジトリにpushすると、自動でGitHub Actionsが実行され、CUnitによりテストされたことが分かると思います1。
他にももっと良いテストフレームワークがあるはず…。
そう、他にももっと良いテストフレームワークがあるはずです。
ここでは、紹介程度に留めますが、例えばこのようなものがあるようです。
- BCUnit…CUnitのforkプロジェクトであり、パッチや機能の追加が行われている
- Cutter…日本人が開発したC/C++向けテストフレームワークであり、ドキュメントも日本語で用意されている
- Google Test…C++向けテストフレームワークであるが、これを使ってC言語のテストも行うことが出来る
おわりに
この記事では、CUnitを使って、C言語のテストを行ってみました。
CUnitはよく出来ているのですが、ところどころに使いにくい点があることは否めない印象でした。
ですが、導入と使いやすさではとても簡単に使えるので、初めてのテストにはちょうど良いかもしれません。
そんな人が、ちょっと踏み込んだ使い方をしてみたいと思った際に、この記事が助けになれば、幸いです。
この先、C言語でテストを行うときが来るかはわかりませんが、その時は、もっと楽に賢く使えるものを使っていきたいですね。
初めてテストを書きましたが、ソースコードを書き換えたとしても、正しく動いていることがすぐに確認できる重要さを凄く感じました。
他の言語でも、テストは積極的に書いていこうと思います。
参考文献
- CUnit Progammers Guide
- CUnit使ってみた
- 20年物のC言語で作られたシステムのテスト工程を改善しようとした話
- CUnitについての備忘録
- Unit-testing static functions in C
- man gcov
- BCUnit
- Cutter
- Google Test
- 
いくつか警告が出ているのでそれについて触れておくと、 format not a string literal and no format argumentsはセキュリティに関する警告でこのバグを悪用される可能性がある、というものです。profiling: ... :Version mismatchはカバレッジ計測によるものです。生成されるtest.gcdaにはカバレッジ計測の結果が上書きされるのではなく、蓄積されていきます。なので、実行ファイルが異なる場合には蓄積できません。一度test.gcdaを削除してからやり直すことで解決します。今回は、出力結果のサンプルとしてGitHubのリポジトリ内にファイルを追加したのでこのようなエラーが表示されますが、通常、.gcdaなどは.gitignoreに追加し、追跡しないようにしておきます。 ↩
