昨日、C言語用の単体テストフレームワークである Cutterの1.0.3がリリースされま した。
実は、Cutter-1.0リリースから3回リリースしていま す。1.0.0以降はマイクロバージョンだけを上げていますが、新しく 追加された機能はマイクロとは思えません。例えば、Windows (MinGW)でのビルド に対応、GStreamer のサポートなどといった機能が含まれていました。過去のリリースに ついてはNEWS を見てください。
Cutterはテストの書きやすさ・テスト結果からのデバッグのしやす さを重視したC言語用の単体テストフレームワークです。今回のリリー スからCutterの機能を説明したページ を用意 しました。
同じテストを条件を変えて実行したい時があります。例えば、以下 のような場合です。
このような場合、必要な分だけテストコードをコピー&ペーストして テストを作成するよりも、以下のように書けるとテスト記述・管理 のコストを下げることができます。
このようなテストの方法をデータ駆動テストと呼びます。
データ駆動テストではデータの用意の仕方にはいくつかの方法があ り、それぞれ利点があります。
Cutterでは今回のリリースで、最後の「プログラム内で入力データ を生成」する方法をサポートしました。使い方は以下の通りです。
今までどおり、関数を定義するだけでよく、他のC言語用の単体テ ストフレームワークにあるような「登録処理」のようなことは必要 ありません。Cutterが自動で見つけてくれます。
コードにすると以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void data_XXX(void) { cut_add_data("データ1の名前", data1, data1_free_function, "データ2の名前", data2, data2_free_function, "データの例", strdup("test data"), free, ...) } void test_XXX(const void *data) { /* dataはdata_XXX()で登録した「data1」か「data2」 か「strdup("test data")」。test_XXX()はそれぞれに対 して1回ずつ、計3回呼ばれる。 */ cut_assert_equal_string("test data", data); } |
具体例は cut_add_data() を見てください。
Cutter 1.0.3ではデータ駆動テストをサポートし、より簡単にテス トがかけるようになりました。
Sennaの単体テストフレームワー クとしてCutterを導入したときの手順です。自分のプロジェクトに Cutterを導入するときの参考になるかもしれません。全体として そこそこ長くなってしまったので、何回かに分割して紹介することに します。
内容はSennaのリポジトリ でやったことの一部です。リポジトリは公開されているので、試行錯誤の 後などをみたい場合はコミットを追いかけるとよいでしょう。また、ここで は断片としてしか出てこないコードについても、リポジトリの中には完全な 形で入っています。
もし、まだCutterについて知らない場合は、はじめにチュートリ アル を読んでください。
まず、Sennaについて簡単に説明します。
Sennaは組み込み型の全文検索エンジンで、その機能をライブラリ として提供します。SennaのAPIはbasic APIやadvanced APIなどい くつかのグループにわかれています。
今回はSennaの単体テストフレームワークとしてCutterを導入し、 utility APIのひとつ、snippet*1のテストを 作成するまでを示します。このためには以下の作業が必要になりま す。
作業に入る前にSennaのビルドシステムについて確認します。
SennaではGNU Automakeや GNU Libtoolな どGNUビルドシステムを利用したビルドシステムを採用しています。
CutterはGNUビルドシステムサポート用の機能をいくつか提供してい ます。そのため、GNUビルドシステムを用いているプロジェクトへ はCutterを容易に導入することができます。
もし、これからプロジェクトを始める場合でGNUビルドシステムを 採用する場合はCutterのチュートリアル が参考になるでしょう。
Sennaの単体テストフレームワークとしてCutterを採用するにあたっ て、以下のような条件を満たすこととします。
上記の中でのユーザと開発者の違いは、autogen.shを用いて自分で configureを作成するかどうかです。ユーザは開発者が作成した configureを利用するため、自分でconfigureを作成しません。一方、 開発者はSubversionリポジトリ内にはconfigureは入っていないの でautogen.shを使ってconfigure.acからconfigureを作成し、利用 します。つまり、違いは以下の通りになります。
それでは、まずは、開発者はすべてCutterをインストールしている ものとしてCutter対応のconfigureを生成できるようにします。
Cutterはconfigure.ac内で利用できるCutter検出用のM4マクロを cutter.m4として提供しています。このファイルは ${PREFIX}/share/aclocal/cutter.m4としてインストールされます。 ${PREFIX}/share/aclocal/以下に他の.m4ファイルがインストールされ ているような環境ではおそらくそのままで大丈夫ですが、そうでな い場合はautogen.shの中でaclocalを呼び出しているところを編集 して${PREFIX}/share/aclocal/以下を.m4ファイルの検索パスに加 える必要があります。
もし、Cutterのconfigureに--prefix=/tmp/localオプションをつけ てビルド・インストールした場合はautogen.shを以下のように変更 する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Index: autogen.sh =================================================================== --- autogen.sh (リビジョン 820) +++ autogen.sh (作業コピー) @@ -105,7 +105,7 @@ echo "Running libtoolize ..." $LIBTOOLIZE --force --copy echo "Running aclocal ..." -$ACLOCAL ${ACLOCAL_ARGS} -I . +$ACLOCAL ${ACLOCAL_ARGS} -I . -I /tmp/local/share/aclocal echo "Running autoheader..." $AUTOHEADER echo "Running automake ..." |
あるいはautogen.shを実行する時に環境変数ACLOCAL_ARGSを指定し ます。
% ACLOCAL_ARGS="-I /tmp/local/share/aclocal" ./autogen.sh
これでconfigure.ac内でCutterが提供する便利M4マクロを利用する 準備が整いました。
Cutterはパッケージを pkg-configのパッ ケージとしてインストールします。パッケージをpkg-configのパッ ケージとして作成しているのは、pkg-configが広く普及していて、 GNUビルドツールなどpkg-configに対応しているビルドシステムが 多いからです。
Cutterは、テスト作成用に以下の2つのパッケージを用意しています。
今回はGLibを利用してテストを作成するので、cutterパッケージで はなくgcutterパッケージを利用します。
Cutterはconfigure.acで簡単にcutter/gcutterパッケージの設定を 行えるように以下のM4マクロを提供しています。
cutterパッケージ検出マクロです。以下の変数をAC_SUBSTしま す。
また、cutterパッケージが利用不可能な場合は ac_cv_use_cutterが"no"になります。
今回はGLibサポートがついたgcutterパッケージを利用するので、 AC_CHECK_GCUTTERマクロを利用します。よってconfigure.acには以 下を追加することになります。
configure.ac:
1 2 3 4 5 6 |
AC_CHECK_GCUTTER AM_CONDITIONAL([WITH_CUTTER], [test "$ac_cv_use_cutter" != "no"]) if test "$ac_cv_use_cutter" != "no"; then AC_DEFINE(WITH_CUTTER, 1, [Define to 1 if you use Cutter]) fi |
これで、Makefile.amではCutterが利用できるかどうかはif WITH_CUTTER ... endifで判断できます。Makefile.amではCutterが 利用できない場合はテストプログラムをビルドしないようにします。 こうすることにより、ユーザがCutterをインストールしていなくて も、Sennaをビルドできます。
cutter.m4がない場合は./autogen.shの実行が失敗します。つまり、 開発者がconfigureを正常に生成できなくなります。
残念ながら、Cutterはそれほど有名なフリーソフトウェアではない ため、開発者がCutterをインストールしていることはほとんどあり ません。そこで、開発者がCutterをインストールしていなくても configureを生成できるようにします。*2
cutter.m4がインストールされているかどうかはAC_CHECK_GCUTTER 関数が定義されているかどうかでわかります。そのため、以下のよ うに書くことにより、Cutterがインストールされてない環境でも configureを生成できます。もちろん、生成されたconfigureには Cutterの検出機能などはありません。
configure.ac:
1 2 3 4 5 6 7 8 9 |
m4_ifdef([AC_CHECK_GCUTTER], [ AC_CHECK_GCUTTER ], [ac_cv_use_cutter="no"]) AM_CONDITIONAL([WITH_CUTTER], [test "$ac_cv_use_cutter" != "no"]) if test "$ac_cv_use_cutter" != "no"; then AC_DEFINE(WITH_CUTTER, 1, [Define to 1 if you use Cutter]) fi |
このようにAC_CHECK_GCUTTERの呼び出し部分をm4_ifdefの中に入れ るだけです。AC_CHECK_GCUTTERが定義されていない場合は ac_cv_use_cutterを"no"にしているのでWITH_CUTTERが真になるこ とはありません。
Cutterを用いたテストプログラムはtest/unit/以下に配置します。 このディレクトリは新規に作成するため、以下の作業が必要になり ます。
まずは、test/Makefile.amのSUBDIRSにunitを追加し、test/unit/ 以下もビルド対象とします。
test/Makefile.am:
1 |
SUBDIRS = unit |
続いて、configure.acのAC_CONFIG_FILESにtest/unit/Makefileを 追加し、configureがtest/unit/Makefileを生成するようにします。
configure.ac:
1 |
AC_CONFIG_FILES([... test/unit/Makefile ...]) |
最後に、test/unit/Makefile.amを作成し、test/unit/以下のビル ド方法を設定します。とりあえず、今は空っぽでかまいません。
% touch test/unit/Makefile.am
これで、test/unit/以下をSennaのビルドシステムに加えることがで きました。再度./autogen.sh, ./configureを実行してからmakeす れば、test/unit/以下もビルド対象になっていることがわかります。
% ./autogen.sh % ./configure % make ... make[3]: ディレクトリ `.../test/unit' に入ります ...
test/unit/以下がビルド対象に加わったので、test/unit/以下に作 成するテストプログラムを起動するコマンドを作成します。このテ スト起動コマンドはmake checkから呼び出されることになります。
テスト起動コマンドは伝統的にrun-test.shというシェルスクリプ トになっています。このシェルスクリプトからcutterコマンドを呼 び出してテストを実行します。
cutterを実行するときはいくつかオプションを指定する必要があり ます。例えば、テストプログラムがあるディレクトリなどがそれで す。ここでrun-test.shを作成する理由は、cutterへ渡すオプション などを指定しなくてもよいようにするなど、より簡単にテストを実 行できるようにするためです。
テストが簡単に実行できるということはとても重要なことです。テ ストを実行することが面倒だと、だんだんテストを実行しなくなっ てしまうからです。テストが実行されないと、新しくテストを作成 することも面倒になってくるでしょう。これは悪い循環といえます。 これを防ぐためにも最初のうちから簡単にテストを実行できる仕組 みを用意しておくことが重要です。
また、引数なしでも動くrun-test.shを用意することにはもう一つ理 由があります。それは、GNU Automakeが提供するテスト起動の仕組 みであるmake checkからも利用できるようにすることです。make checkでは指定されたテスト起動スクリプトが引数なしでテストを実 行できる必要があります。*3
前置きが長くなりましたがテストをもっと簡単に走らせるためのス クリプト、run-test.shは以下のようになります。
test/unit/run-test.sh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#!/bin/sh export BASE_DIR="`dirname $0`" if test x"$NO_MAKE" != x"yes"; then make -C $BASE_DIR/../../ > /dev/null || exit 1 fi if test -z "$CUTTER"; then CUTTER="`make -s -C $BASE_DIR echo-cutter`" fi if test x"$CUTTER_DEBUG" = x"yes"; then CUTTER="$BASE_DIR/../../libtool --mode=execute gdb --args $CUTTER" fi CUTTER_ARGS="-s $BASE_DIR" $CUTTER $CUTTER_ARGS "$@" $BASE_DIR |
このスクリプトではmake check以外からも便利に利用できるように なっています。make check以外から起動された場合(つまり直接 test/unit/run-test.shを軌道した場合)は必要なビルドを行ってか らテストを起動します。つまり、run-test.shからテストを起動した 場合はビルド忘れがなくなります。
実は、上記のrun-test.shを直接起動できるようにするためには、 test/unit/Makefile.amにも一工夫する必要があります。それは、 configureで検出したcutterコマンドのパスをrun-test.shに伝える ためのターゲットを用意するということです。
test/unit/Makefile.am:
1 2 |
echo-cutter: @echo $(CUTTER) |
これで、run-test.shを直接起動しても、必要に応じてビルドした り、情報を集めたりしてテストを起動してくれます。
また、make checkではテスト結果とビルド結果が混ざりそこそこの 出力になりますが、run-test.sh経由でビルド・テストを行うと必 要最小限の出力になり、問題の発見が簡単になります。実際の開発 は以下のようなサイクルになります。
test/unit/run-test.shを実行
テスト失敗→(1)に戻る
手順が少ないため開発のリズムが崩れにくくなります。このサイク ルをより簡単に行うための方法もあるのですが、それはまた別の機 会にします。
run-test.shができたので、make checkでrun-test.shを起動するよ うにMakefile.amを変更します。
test/unit/Makefile.am:
1 2 3 4 5 |
if WITH_CUTTER TESTS = run-test.sh TESTS_ENVIRONMENT = NO_MAKE=yes ... endif |
TESTS_ENVIRONMENTにNO_MAKE=yesを指定することにより、make check経由の場合はテスト実行前のmake実行を抑制します。
これでテストを実行するための環境は整いました。きりがよいので 今回はここまでにします。
ここまでで、以下のことについて説明しました。
続きではテストを作成します。
*1 検索キーワードの周辺テキストの こと。ここではそれを取得するSennaの機能のこと。
*2 本当は開発者には頻繁に テストを走らせて欲しいのでCutterを必須にしたいところです。
*3 テスト起動スクリプトにオプションを 指定する場合は環境変数を利用します。