CUnit使ってみた

会社でC言語をやることになった。
…のはいいが、バージョン管理とかテスティングフレームワークなし。
さすがにやってられないと思ったので、C言語のテスティングフレームワークが無いものかと探したところ、CUnitというテスティングフレームワークを見つけた。
CUnit Home
そんなわけでこっそり使ってみることに。

導入

まずはソースコードをダウンロード。
で、解凍して./configureしてmakeしてmake installでインストールできます。
今回はCygwinに導入しましたが、他のLinux環境でも同じようにできるかと思います。
Cygwinを利用する場合、gccとmakeは必須です。
Cygwinのセットアップから追加してください。

使い方

テストスイート作成・テスト登録

CUnitチュートリアルの「テスト環境の概要」ページの「一般的な使用手順」より引用。

CUnit を利用したテスト環境の作成/実行の手順は、一般的に次のようになります。

  1. テストケースを表現する関数を作成します。
  2. 必要に応じてセットアップ関数、クリーンアップ関数を用意します。
  3. テスト・レジストリを作成/初期化します(CU_initialize_registry( ) という関数を呼び出します)。
  4. テスト・スイートをテスト・レジストリに追加します(CU_add_suite( ) という関数を用います)。
  5. テスト・ケースをテスト・スイートに追加します(CU_add_test( ) という関数を用います)。
  6. テストを実行します(例えば、CU_console_run_tests( ) という関数を呼び出します)。
  7. テスト・レジストリを削除します(CU_cleanup_registry( ) という関数を呼び出します)。
CUnitチュートリアル テスト環境の概要

では、おなじみ(?)のFizzBuzzを例としてテストを実行するソースコードの書いてみます。

TestSrc.c
---
#include <CUnit/CUnit.h>
#include <CUnit/Console.h>

int main(void)
{
	CU_pSuite testSuite;
	CU_initialize_registry();
	testSuite = CU_add_suite("FizzBuzzTestSuite", NULL, NULL);
	CU_add_test(testSuite, "FizzBuzzTest", testFizzBuzz);
	CU_console_run_tests();
	CU_cleanup_registry();
	
	return 0;
}

CUnitを使用する為に必要なヘッダファイルは、基本的にはCUnit/CUnit.hとCUnit/Console.hの2つ。
これらをまずはインクルードします。
そして、メイン関数内でテストスイートの宣言(CU_pSuite)、レジストリ作成と初期化(CU_initialize_registry())をします。
その次にテストスイートを登録し(CU_add_suite())、テストスイートにテストケースを追加していきます(CU_add_test())。
CU_add_suite関数の1番目の引数で指定しているテストスイート名、およびCU_add_test関数の2番目の引数で指定しているテストケースの名前は自由に決めることができます。
とはいえ、ここで指定した名前はテスト実行時に表示されるためわかりやすい名前にしたほうがベター。
またCU_add_test関数の3番目の引数で、testFizzBuzzと指定されていますが、これがテストケースです。
テストケースはtest○○という名前の関数で実装します。
テストケースを追加したら次はテストを実行します(CU_console_run_tests())。
そして最後に、レジストリを削除して終了です(CU_cleanup_registry())。

テストケースとなるテスト関数を実装

当然ですが、テストケースとなる関数の実装がないままだと当然動かないので実装します。
たとえばこんな感じ。

TestSrc.c
---
void testFizzBuzz(void)
{
	char result[256] = "";
	fizzbuzz(1, result);
	
	CU_ASSERT_STRING_EQUAL("1", result);
}

今回のFizzBuzzは数値と文字列を引数に取り、文字列に判定結果を代入して返す、という実装とします。
ここではFizzBuzzの実装としてfizzbuzz関数を利用しています。
で、その次のCU_ASSERT_STRING_EQUAL関数(マクロ)、これがアサート文です。
このアサート文では、文字列が一致しているかどうかを判定します。


そして次はテスト対象となるfizzbuzz関数を実装します。

Src.c
---
void fizzbuzz(int num, char *result)
{
}

この時点では仮実装でOK.

ついでにプロトタイプ宣言をヘッダファイルに書いて、Src.cおよびTestSrc.c両方にインクルードしておきます。
インクルードガード込みで書くと以下の通り。

Src.h
---
#ifndef _SRC_H_
#define _SRC_H_

void fizzbuzz(int, char *);

#endif

ここまでできたらコンパイル
このとき、gccにはCUnitのライブラリの参照先を指定しておくこと。

gcc Src.c TestSrc.c -Wall -L/usr/local/lib -lcunit

いちいちタイプするのがめんどくさいなら、シェルスクリプトもしくはMakefileを書いておくと楽です。

実行するとこんな感じ。

$ ./a.exe


     CUnit - A Unit testing framework for C - Version 2.1-2
             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でテストを実行します。

Enter command: r

Running Suite : FizzBuzzTestSuite
     Running Test : FizzBuzzTest

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

Elapsed time =    0.000 seconds


***************** CUNIT CONSOLE - MAIN MENU ******************************
(R)un  (S)elect  (L)ist  (A)ctivate  (F)ailures  (O)ptions  (H)elp  (Q)uit
Enter command:

Totalは合計、Ranは走ったテスト、Passedは通過したテスト(要はGreen)、Failedは失敗したテスト(要はRed)、Inactiveは実行していないテストです。
当然ですが、仮実装なのでテストは失敗します。
失敗した場合、fでどこで失敗したかを確認することが出来ます。

Enter command: f

--------------- Test Run Failures -------------------------
   src_file:line# : (suite:test) : failure_condition

1. TestSrc.c:24 : (FizzBuzzTestSuite : FizzBuzzTest) : CU_ASSERT_STRING_EQUAL("1
",result)
-----------------------------------------------------------
Total Number of Failures : 1


***************** CUNIT CONSOLE - MAIN MENU ******************************
(R)un  (S)elect  (L)ist  (A)ctivate  (F)ailures  (O)ptions  (H)elp  (Q)uit
Enter command:

これで、ソースコードのどこでテストをパスしていないか確認することができます。
では、今度はテストを通るように実装を変更します。
とりあえず受け取ったint型をsprintfで文字列にしてresultに格納する実装にしてみます。

Src.c
---
void fizzbuzz(int num, char *result)
{
	sprintf(result, "%d", num);
}

コンパイルして実行。

$ ./a.exe


     CUnit - A Unit testing framework for C - Version 2.1-2
             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 : FizzBuzzTestSuite
     Running Test : FizzBuzzTest

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

Elapsed time =    0.000 seconds


***************** CUNIT CONSOLE - MAIN MENU ******************************
(R)un  (S)elect  (L)ist  (A)ctivate  (F)ailures  (O)ptions  (H)elp  (Q)uit
Enter command:

アサートでのFailedがなくなり、テストをパスしていることが確認できます。
こんな感じでテストファーストで進めていきます。
ある程度まで書いたコードを参考として載せておきます。
…お世辞にも綺麗なコードとは言いがたい上、0とか負数は全く考慮に入れていませんが書き捨てということで勘弁してください。

TestSrc.h
---
#ifndef _TEST_SRC_H_
#define _TEST_SRC_H_

static void testFizzBuzz(void);

#endif
TestSrc.c
---
#include <CUnit/CUnit.h>
#include <CUnit/Console.h>

#include "Src.h"
#include "TestSrc.h"

int main(void)
{
	CU_pSuite testSuite;
	CU_initialize_registry();
	testSuite = CU_add_suite("FizzBuzzTestSuite", NULL, NULL);
	CU_add_test(testSuite, "FizzBuzzTest", testFizzBuzz);
	CU_console_run_tests();
	CU_cleanup_registry();
	
	return 0;
}

void testFizzBuzz(void)
{
	char result[256] = "";
	fizzbuzz(1, result);
	CU_ASSERT_STRING_EQUAL("1", result);
	
	fizzbuzz(2, result);
	CU_ASSERT_STRING_EQUAL("2", result);
	
	fizzbuzz(3, result);
	CU_ASSERT_STRING_EQUAL("Fizz", result);
	
	fizzbuzz(5, result);
	CU_ASSERT_STRING_EQUAL("Buzz", result);
	
	fizzbuzz(7, result);
	CU_ASSERT_STRING_EQUAL("7", result);
	
	fizzbuzz(9, result);
	CU_ASSERT_STRING_EQUAL("Fizz", result);
	
	fizzbuzz(15, result);
	CU_ASSERT_STRING_EQUAL("FizzBuzz", result);
}
Src.h
---
#ifndef _SRC_H_
#define _SRC_H_

void fizzbuzz(int, char *);

#endif
Src.c
---
#include <stdio.h>

#include "Src.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);
	}
}

利点と欠点

利点は言うまでもなく、C言語でTDDおよびテストファーストができること。
これのおかげでずいぶんコーディングが楽になりました。
C言語の場合強力なライブラリもなく、処理に関しては他の言語よりも注意してひとつひとつチェックしながらコードを書かなければいけないため、危ない橋を渡ることは少なくなりました。
やっぱり少しずつ進めていくことは大事ですね。
欠点は、テストが失敗したときの結果表示のわかりづらさ。
CUnitの場合、テストが失敗した場合、「どこで失敗したか」しか教えてくれない*1ため、デバッグが少しやりづらいです。
JUnitのように値を表示してくれるとありがたいんですが。

他の機能とか

先にもリンク載せましたが、日本語ドキュメントがあります。
他のアサートマクロとその利用方法、他詳しい解説も載っています。
http://homepage3.nifty.com/kaku-chan/cunit/index.html
私もここを参考にしました。

やるかわからないけど次回予告

この後、他のC言語用テスティングフレームワークはないものかとググったところ、cutterなるテスティングフレームワークを発見。
こっちだとテストが失敗したときの値を表示してくれるっぽいです。
次にまたC言語やることになったら試してみるかも。
# あんまりC言語ばっかり書いていたくはないけども…。

*1:それでも無いよりマシ