株式会社クリアコード > ククログ

ククログ


Cutter 1.0.3リリース

昨日、C言語用の単体テストフレームワークである Cutterの1.0.3がリリースされま した。

実は、Cutter-1.0リリースから3回リリースしていま す。1.0.0以降はマイクロバージョンだけを上げていますが、新しく 追加された機能はマイクロとは思えません。例えば、Windows (MinGW)でのビルド に対応、GStreamer のサポートなどといった機能が含まれていました。過去のリリースに ついてはNEWS を見てください。

Cutterとは

Cutterはテストの書きやすさ・テスト結果からのデバッグのしやす さを重視したC言語用の単体テストフレームワークです。今回のリリー スからCutterの機能を説明したページ を用意 しました。

データ駆動テスト対応

同じテストを条件を変えて実行したい時があります。例えば、以下 のような場合です。

  • 複数の入力パターンがあり、それらを網羅的にテストする場合
  • 複数のバックエンドを抽象化し、どのバックエンドを利用して いる場合でも同じインターフェイスで扱えるライブラリをテス トする場合(Cでのcairo、Perl/Ruby/GaucheなどでのDBI、 RubyでのActiveRecordなど)

このような場合、必要な分だけテストコードをコピー&ペーストして テストを作成するよりも、以下のように書けるとテスト記述・管理 のコストを下げることができます。

  • テストは1つだけ用意
  • テスト条件、つまり、入力データを複数用意
  • 各入力データに対してそれぞれテストを実行

このようなテストの方法をデータ駆動テストと呼びます。

データ駆動テストではデータの用意の仕方にはいくつかの方法があ り、それぞれ利点があります。

データベースに保存された入力データを利用
大量のデータを用意したり、データを一括変更できるなどデー タ管理機能が豊富
CSVなど表形式の入力データを利用
Excelなどを利用して入力データを用意することができる
プログラム内で入力データを生成
動的にデータを用意するので、柔軟にデータを生成すること ができる。例えば、文字'a', 'b', 'c'を使って作られる長さ が3の文字列すべて("abc", "acb", ...)、などというデータ を用意できる。

Cutterでは今回のリリースで、最後の「プログラム内で入力データ を生成」する方法をサポートしました。使い方は以下の通りです。

  • data_XXX(void)を定義
  • data_XXX()中でcut_add_data()を使ってデータを登録
  • test_XXX(const void *data)を定義
    • dataにはcut_add_data()で登録したデータの1つが渡る

今までどおり、関数を定義するだけでよく、他の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ではデータ駆動テストをサポートし、より簡単にテス トがかけるようになりました。

タグ: Cutter | テスト
2008-07-16

Cutter導入事例: Senna (1)

Sennaの単体テストフレームワー クとしてCutterを導入したときの手順です。自分のプロジェクトに Cutterを導入するときの参考になるかもしれません。全体として そこそこ長くなってしまったので、何回かに分割して紹介することに します。

内容はSennaのリポジトリ でやったことの一部です。リポジトリは公開されているので、試行錯誤の 後などをみたい場合はコミットを追いかけるとよいでしょう。また、ここで は断片としてしか出てこないコードについても、リポジトリの中には完全な 形で入っています。

もし、まだCutterについて知らない場合は、はじめにチュートリ アル を読んでください。

はじめに

まず、Sennaについて簡単に説明します。

Sennaは組み込み型の全文検索エンジンで、その機能をライブラリ として提供します。SennaのAPIはbasic APIやadvanced APIなどい くつかのグループにわかれています。

今回はSennaの単体テストフレームワークとしてCutterを導入し、 utility APIのひとつ、snippet*1のテストを 作成するまでを示します。このためには以下の作業が必要になりま す。

  • SennaのビルドシステムにCutterを組み込む
  • Cutterでsnippet APIのテストを記述する

作業に入る前にSennaのビルドシステムについて確認します。

Sennaのビルドシステム

SennaではGNU AutomakeGNU Libtoolな どGNUビルドシステムを利用したビルドシステムを採用しています。

CutterはGNUビルドシステムサポート用の機能をいくつか提供してい ます。そのため、GNUビルドシステムを用いているプロジェクトへ はCutterを容易に導入することができます。

もし、これからプロジェクトを始める場合でGNUビルドシステムを 採用する場合はCutterのチュートリアル が参考になるでしょう。

ビルドシステムへのCutterの組み込み

Sennaの単体テストフレームワークとしてCutterを採用するにあたっ て、以下のような条件を満たすこととします。

  • Cutterがない場合でもユーザがSennaをビルドできること
  • Cutterがない場合でも開発者がSennaをビルドできること
  • configure時にCutterを使用するかどうかを指定できること
  • Cutterで作成したテストはtest/unit/以下に配置すること
  • テストではテストを簡潔・容易に記述するためにGLibを利用す る

上記の中でのユーザと開発者の違いは、autogen.shを用いて自分で configureを作成するかどうかです。ユーザは開発者が作成した configureを利用するため、自分でconfigureを作成しません。一方、 開発者はSubversionリポジトリ内にはconfigureは入っていないの でautogen.shを使ってconfigure.acからconfigureを作成し、利用 します。つまり、違いは以下の通りになります。

  • ユーザ: configureのみ実行
  • 開発者: autogen.shとconfigureを実行

それでは、まずは、開発者はすべてCutterをインストールしている ものとしてCutter対応のconfigureを生成できるようにします。

cutter.m4

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つのパッケージを用意しています。

  • cutter: Cutterを利用してテストを作成する場合に利用
  • gcutter: cutterパッケージにGLibサポート機能を追加したパッ ケージ。GLibを利用してもっと簡潔・容易にテストを書きたい 場合に利用

今回はGLibを利用してテストを作成するので、cutterパッケージで はなくgcutterパッケージを利用します。

Cutterはconfigure.acで簡単にcutter/gcutterパッケージの設定を 行えるように以下のM4マクロを提供しています。

AC_CHECK_CUTTER

cutterパッケージ検出マクロです。以下の変数をAC_SUBSTしま す。

  • CUTTER: cutterコマンドのパス
  • CUTTER_CFLAGS: cutterパッケージを用いたテストをビルド するためのCFLAGS
  • CUTTER_LIBS: cutterパッケージを用いたテストをビルド するためのLIBS

また、cutterパッケージが利用不可能な場合は ac_cv_use_cutterが"no"になります。

AC_CHECK_GCUTTER
gcutterパッケージ検出マクロです。上述のAC_CHECK_CUTTERマ クロがAC_SUBSTする変数に加えて、以下の変数もAC_SUBSTしま す。
  • GCUTTER_CFLAGS: gcutterパッケージを用いたテストをビル ドするためのCFLAGS
  • GCUTTER_LIBS: gcutterパッケージを用いたテストをビルド するためのLIBS

今回は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がない場合への対応

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が真になるこ とはありません。

ビルドシステムへtest/unit/以下を追加

Cutterを用いたテストプログラムはtest/unit/以下に配置します。 このディレクトリは新規に作成するため、以下の作業が必要になり ます。

  • test/Makefile.amのSUBDIRSにunitを追加
  • configureでtest/unit/Makefileを生成する設定を追加
  • test/unit/Makefile.amの作成

まずは、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経由でビルド・テストを行うと必 要最小限の出力になり、問題の発見が簡単になります。実際の開発 は以下のようなサイクルになります。

  1. ソース変更
  2. test/unit/run-test.shを実行

    テスト失敗→(1)に戻る

  3. コミット→(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実行を抑制します。

これでテストを実行するための環境は整いました。きりがよいので 今回はここまでにします。

まとめ

ここまでで、以下のことについて説明しました。

  • GNUビルドシステムを採用した既存のプロジェクトへのCutter の組み込み方法
    • cutter.m4で提供するM4マクロの使用方法
    • Cutterをインストールしていないユーザへの対応
    • Cutterをインストールしていない開発者への対応
  • Cutterを用いたテスト環境の構築方法
    • 便利なテスト起動スクリプトrun-test.shの作成方法

続きではテストを作成します。

*1 検索キーワードの周辺テキストの こと。ここではそれを取得するSennaの機能のこと。

*2 本当は開発者には頻繁に テストを走らせて欲しいのでCutterを必須にしたいところです。

*3 テスト起動スクリプトにオプションを 指定する場合は環境変数を利用します。

タグ: Cutter | テスト
2008-07-25

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|