ここでは,GNU Make を使った JUnit 開発環境の話をしたいと思います.想定している読者は, Eclipse や JBuilder といった IDE ではなく,Meadow など Emacs 系エディタで Java SDK をそのまま使うような開発者です.
最近は Ant を使っている人が多いようですが,JUnit を使ったテストファーストの開発において Ant の優位性はあまりありません.もちろん, Ant はプロジェクト全体に対する作業には向いています.つまり,開発したものを jar ファイルにする,プロジェクト全体のテストを行なう…といった作業です.しかし,テストファーストでは,これ以外に次のようなことが簡単にできなければなりません.
もしこういったことができず,プロジェクト全体のテストしか行なえないとすると,ある程度規模が大きいプロジェクトで test a little, code a little, ... なんてことはできません.テストが終わるまで 10分以上かかるのに,どうしてわざわざテストケースを失敗させ,テストケースがちゃんと機能しているか確認…みたいな悠長なことができるのでしょうか? make を使えば,こういった細かい単位でのテストが可能です.普段の開発は make で,配布するときや全体の作業は Ant で,という使い分けを行なうのが一番いいでしょう.これまで Ant が便利と思っていた人も,この記事を読んで make の良さを再確認していただければと思います.
現在僕が使っている環境は,
です.GNU Make は Cygwin に同梱されているものを使っています( GNU Make は Cygwin の make コマンドです).また,Meadow の shell は bash です.他の環境では試したことがありませんのでご了承ください.Java SDK については,バージョン間で javac と java のコマンドラインオプションにそれほど違いがないので無関係だと思います.
というわけで,GNU Make を利用するために Cygwin がインストールされていなければなりません.必要な方は, Cygwin をインストールしておいてください.また,数年前の古い Cygwin の make にはバグがあったと思うので,あまりに古い Cygwin を使っている方も最新のものにバージョンアップしてください.
次に,環境変数 PATH を書き換えて Cygwin の /bin (つまり [Cygwin インストール先]\bin )が Windows システムディレクトリより先に来るようにしておきます.これは, Windows の find コマンドと Cygwin の find コマンドがかち合ってしまうためです.もちろん, Windows の バッチファイルを使い込んでいて Windows の find コマンドを使っている人は問題かもしれませんので,その辺の判断はお任せします(未確認ですが,以下のライブラリは Windows システムディレクトリ優先の PATH 設定でも機能するよう作っているつもりです).
ここでは,メインとなる make ライブラリ java.mk のインストール方法について説明します.
まず,次の 3つのファイルをダウンロードしてください.
ファイル名 説明 保存先 java.mk JUnit 開発環境の make ライブラリ本体. /usr/local/include subdirs.mk java.mk が利用するディレクトリ階層を扱う make ライブラリ. /usr/local/include java-mk-lib.jar java.mk が利用する java クラスファイル. 任意の場所
表にあるように, java.mk と subdirs.mk は /usr/local/include ディレクトリに保存しておきます [1].
Cygwin に詳しくない方へ: Cygwin ではルートディレクトリ: / は Cygwin のインストール先と解釈されます.例えば, Cygwin を c:\cygwin にインストールした場合, /usr/local/include は c:\cygwin\usr\local\include になります.逆に,c ドライブなどドライブの指定は(特別な設定をしない限り) /cygdrive/c/ となります.したがって c:\temp ディレクトリは /cygdrive/c/temp となります.Cygwin を使う場合はこのことに注意しておいて下さい.
次に,ユーザ側で用意しなければならないファイルがあります.
ファイル名 説明 保存先 config.mk JUnit のクラスパスなど,ユーザの環境設定を行なう make ファイル /usr/local/include
config.mk も /usr/local/include に保存します.上のファイルをダウンロードし,自分の環境に合わせて JUnit のクラスパスと java-mk-lib.jar のクラスパスを書き換えて下さい.
最後に MAKEFILES 環境変数を設定します.この環境変数にファイル名を設定しておけば,make が起動されたときそのファイルが自動的に読み込まれます. ここに先ほど作った config.mk のフルパスを指定しておきます.
環境変数 値 MAKEFILES /usr/local/include/config.mk
もし設定しなかった場合は config.mk が自動的にインクルードされないため,java.mk を使うと
*** JAVA_MK_LIB_CLASSPATH が定義されていません. Stop.
というエラーメッセージが出ます.もっとも, MAKEFILES 環境変数を設定せず各 Makefile で明示的に
include config.mk
とすることもできますが,ここでは MAKEFILES が設定されているものとして話を進めます.
以上でライブラリのインストールは終りです.
最初は 1つのディレクトリのみで開発を行なう簡単なプロジェクトを考えてみましょう.
まず,以下の内容の Makefile を作成します.
include java.mk
たった一行です.とりあえずこれだけで開発を始めることができます.
さて, Makefile と同じディレクトリに Foo.java というファイルを作り, Foo クラスを実行したいときは [クラス名].run ターゲットを使います.
$ make Foo.run
これだけで Foo クラスが実行されます.なお,ここではシェルコマンドを実行することを表すため,横に $ というプロンプトを書いています.
Emacs 系エディタを使っている方へ: Emacs ではシェルで実行するより M-x compile コマンドを使うほうが便利でしょう.M-x compile コマンドを実行した後,ミニバッファに make Foo.run と入力してください.ところで,あまり知られてないことですが,ミニバッファにはヒストリ機能があります.つまり, M-p や M-n で以前入力した compile コマンドの履歴を行き来できます.この機能を使えば面倒な入力は必要最小限ですむでしょう.
こうすると, java.mk は以下のコマンドを実行します.
javac -classpath "[クラスパス]" -deprecation ./Foo.java java -cp "[クラスパス]" Foo
ここで, [クラスパス]には JAVA_MK_CLASSPATH マクロに設定されている値が入ります.デフォルトでは,カレントディレクトリと $(JUNIT_CLASSPATH) の値が入っていますが,これを変えたい場合は Makefile を次のように書き換えてください.
JAVA_MK_CLASSPATH = .;c:/oracle/lib/classes12_01.zip;$(JUNIT_CLASSPATH) include java.mk
また,コマンドライン引数を指定して実行したい場合は, RUN_ARGUMENT マクロを指定します.
$ make Foo.run RUN_ARGUMENT="arg1 arg2"
こうすると Foo クラスの main メソッドに arg1 arg2 という二つの引数が渡されます.
java.mk にはヘルプ機能がついています.この機能を利用するには Makefile に help ターゲットを追加します.
include java.mk help:$(HELP_TARGETS)
help ターゲットは java.mk をインクルードした後に追加するようにしてください.こうしておけば,
$ make help
で java.mk のヘルプが一覧表示されます.一度実行して一通り確認しましょう.必要な説明はすべて help に書かれていると思います.
さて,今度は JUnit のテストケースを作成し実行してみましょう.ここでは, FooTest.java が次のようになっているとします.
public class FooTest extends junit.framework.TestCase
{
public FooTest(String name) { super(name); }
public void testA() { /* testA の内容 */ }
public void testB() { /* testB の内容 */ }
public void testC() { /* testC の内容 */ }
}
ここで FooTest クラスが main メソッドを持っていないことに注意してください. java.mk を使えば,各テストケースで main メソッドを用意する必要はありません.この FooTest を実行するには, 次のコマンドを実行します.
$ make Foo.test
これで 3つのテストメソッド testA, testB, testC が実行されます.このとき,自動的に次の 2つのコマンドが実行されていることになります.
javac -classpath "[クラスパス]" -deprecation ./Foo.java ./FooTest.java java -cp "[クラスパス];[java-mk-lib.jarのクラスパス]" junit.textui.MultiTestRunner FooTest
コンパイル後 java.mk は junit.textui.MultiTestRunner クラス(後述)を使ってテストケースを実行します.
さて,ここで注意していただきたいのは,
テストケースのクラス名を必ず *Test という形式にする
ということです. java.mk を使った開発では,このネーミングルールを変更することはできません.
FooTest.testA のみ実行したい場合は,TEST_METHOD マクロ に testA を指定して .test ターゲットを実行します.
$ make Foo.test TEST_METHOD=testA
testA と testC のみを実行したい場合は次のようにします.
$ make Foo.test TEST_METHOD="testA testC"
空白が含まれるので ダブルクォートで囲むようにします.さらに -r オプション を使ってテストケースを繰り返し実行することもできます.
$ make Foo.test TEST_METHOD="-r 10"
こうすると, FooTest を 10回繰り返し実行します.他にも
$ make Foo.test TEST_METHOD="-r 10 testA testC"
とすれば FooTest.testA FooTest.testC が10回繰り返して実行され,
$ make Foo.test TEST_METHOD="\(-r 10 testA \) testC"
とすれば FooTest.testA が 10回,FooTest.testC が 1回実行されます.つまり, -r オプションは \( と \) で括った範囲を指定回数分だけ繰り返し実行します.ここで括弧を \ でエスケープしているのは ( と ) がシェルで特別な意味をもつためです.
ディレクトリにあるすべてのテストケースを実行するには, test ターゲットを使います.
$ make test
こうすれば,そのディレクトリにある *Test.java という名前の java ファイルを自動的に検索して実行します.わざわざ AllTests.java というファイルを作る必要はありません.
これまでに紹介した以外の標準的なターゲットをまとめて起きます.
ターゲット名 効果 clean クラスファイルをすべて削除します. build コンパイルしてクラスファイルを作成します. rebuild クラスファイルをすべて削除してから作り直します. retest rebuild した後テストを行ないます.
この内一番よく使われるのは retest ターゲットでしょう.全体に影響を及ぼすような修正を行なった後や,リポジトリにファイルをチェックインするときは retest ターゲットでテストが通るかどうか確認することになるでしょう.
java を使ったプロジェクトでは,通常パッケージ名に対応する深いディレクトリ構成で開発を行うことになります.ここでは,ディレクトリ階層で java.mk を利用する方法について解説しましょう(ここでの解説は java.mk が利用している subdirs.mk の使い方とほぼ同じです.以前 nmake 版 subdirs.mk の解説記事を書いておいたので,できればそちらの記事も参考にしてください).
ここでは,
というディレクトリ階層を考えましょう.このとき,ユーザは次の2種類の make ファイルを作成する必要があります.
ファイル名 書くこと 保存先 common.mk 各ディレクトリ共通のことがら ルートディレクトリ (project) Makefile 各ディレクトリに特化したことがら すべてのディレクトリ
(project, dir1, dir2, dir3, dir3a, dir3b)
つまり,この例では common.mk × 1 と Makefile × 6 の計 7 つの make ファイルをつくることになります.
まず,common.mk はすべてのディレクトリ共通の事柄を書くので.単一ディレクトリのときの Makefile と同じような内容になります.つまり, java.mk をインクルードし,共通のクラスパスを設定しなければなりません.例えば次のようになります.
# common.mk の内容
JAVA_MK_CLASSPATH = $(ROOT_DIR);c:/oracle/lib/classes12_01.zip;$(JUNIT_CLASSPATH)
include java.mk
help: $(HELP_TARGETS)
ここで ROOT_DIR マクロは各 Makefile で設定する項目です.ROOT_DIR に各ディレクトリから見たディレクトリ階層ルートへの相対パスを設定します.この ROOT_DIR を含め, 各 Makefile には次の 3行を書くことになります.
# Makefile の内容 ROOT_DIR = [ディレクトリ階層のルートからの相対パス] SUBDIRS = [サブディレクトリのリスト] #<==この行はサブディレクトリがない場合削除してよい include $(ROOT_DIR)/common.mk
これだけです.例えば project ディレクトリにある Makefile は
# Makefile(project) の内容
ROOT_DIR = .
SUBDIRS = dir1 dir2 dir3
include $(ROOT_DIR)/common.mk
となりますし,dir1 のMakefile はサブディレクトリがないので
# Makefile(dir1) の内容
ROOT_DIR = ..
include $(ROOT_DIR)/common.mk
の 2行,dir3 の Makefile は
# Makefile(dir3) の内容
ROOT_DIR = ..
SUBDIRS = dir3a dir3b
include $(ROOT_DIR)/common.mk
のようになります.java.mk (正確には subdirs.mk )に 各ディレクトリにおく Makefile を自動生成するインストーラのようなものがあればよかったのですが,まだ実装するに至ってません.
ターゲットは単一ディレクトリの場合と同じものが使えます.例えば, project ディレクトリで
$ make test
とすればすべてのテストケースが実行されますし, dir3 ディレクトリでは dir3, dir3/dir3a, dir3/dir3b ディレクトリにあるすべてのテストケースが実行されます.ここでも AllTests.java のようなものを用意する必要はありません.
.test ターゲットも同様です.各ディレクトリにある特定のテストケースを実行したい場合は
$ make Foo.test
などとしてください.また,各テストクラスのネーミングは *Test という形式にしておくことを忘れないでください.
ところで, java パッケージは ROOT_DIR から自動的に推論しています.特に指定する必要はありません.例えば,dir3/dir3a ディレクトリで
$ make JAVA_PACKAGE.help
という help ターゲットを実行すれば次のようなメッセージが表示されます.
JAVA_PACKAGE
- カレントディレクトリのパッケージ名を指定します.
値: dir3.dir3a
このメッセージから,java.mk はこのディレクトリのパッケージを dir3.dir3a と解釈しているのがわかります.
これまで紹介したmakeコマンドを入力するのに便利なEmacs用ライブラリ junit.el を作りました.自由にダウンロードして使ってください.以下のコマンドが使えるようになります.
あるテストケースのみを実行したい場合に使います.このコマンドをFoo.javaまたはFooTest.javaという名前のバッファ上で実行すると
make Foo.test
という引数でM-x compileコマンドを呼び出します.それ以外のバッファでは
make retest
という引数になります.
あるテストメソッドのみを実行したい場合に用います.このコマンドをFooTest.javaファイルのtestBarメソッドの宣言部(public void testBar())より後にカーソルがある状態で実行すると
make Foo.test TEST_METHOD=testBar
という引数でM-x compileコマンドを呼び出します.もしテストメソッドがなければ
make Foo.test
という引数でM-x compileコマンドを呼び出します.あとはM-x junit-runと同じです.
以上で一通り使い方の説明は終わりました.しかし, make でどうしてこんなことができるか不思議に思ってる人や java.mk をカスタマイズして使いたい人もいるかと思います.ここでは, java.mk で使っているテクニックについて簡単に紹介しましょう.
GNU Make には,ファンクションと呼ばれる強力な機能があります.java.mk では,この機能を使ってユーザが余計な設定をしなくてすむよう作られています.例えば, JAVA_PACKAGE を求める部分を引用してみましょう.
JAVA_PACKAGE = $(subst /,.,$(subst :$(shell cd $(ROOT_DIR); pwd)/,,:$(shell pwd)))
ここに登場している $(subst ...) と $(shell ...) が GNU Make のファンクションです.他にもいろいろなファンクションがありますが,ここではこの 2つのファンクションを間単に紹介しておきましょう.
ファンクション 機能 $(subst from,to,text)text にある from をすべて to に置換する. $(shell command)シェルコマンド command の実行結果を返す.
JAVA_PACKAGE が行なっていることは,カレントディレクトリの絶対パス( $(shell pwd) )から ROOT_DIR の絶対パス( $(shell cd $(ROOT_DIR); pwd)/ )を消去し,ディレクトリの区切り文字 / をパッケージの区切り文字 . に置換しているということです.: が絶対パスの先頭についていますが, $(subst ...) で消去するとき必ず頭から消去するようファイル名に使えない文字 : を使っています.時間のある方は,なぜこれがパッケージになるのか考えてみてください.
java 開発用の Makefile では,クラスファイルのビルドに
.java.class: javac -classpath "$(JAVA_MK_CLASSPATH)" $<
というサフィックスルールが使われているのをよく見かけます.こうすると java ファイル一つ一つに対して javac コマンドを起動することになり,ビルドには相当な時間がかかってしまいます.C/C++ の場合はこれで十分でも, java の場合は遅くて使い物になりません.
java.mk では,すべての java ファイルを一括でビルドします. つまり, javac コマンドにコマンドライン引数として渡すのは,必要なすべての java ファイルです.こうすると,どんなに java ファイルが増えても起動する javac は一つだけになり,コンパイル時間が少なくて済みます.
それでは,どうやって必要な java ファイルを集めるかですが,ここで find コマンドが登場することになります.find コマンドは,あるディレクトリ以下のファイルを検索するのに非常に便利なコマンドです. java.mk から該当する部分を抜き出してみましょう(わかりやすくするため,一部修正しています).
JAVAC_SRC_FILES=$(shell $(FIND) -name '*.java' -and -not -path '*/CVS/*' $(JAVAC_FIND_FLAGS) -print)
これは, JAVAC_SRC_FILES マクロに値を設定する式です.$(shell ...) は先ほど出てきたシェルコマンドのファンクションで, FIND マクロは find コマンド, JAVAC_FIND_FLAGS はデフォルトで空です(カスタマイズ用のマクロ.後述).つまり上の式は,
$ find -name '*.java' -and -not -path '*/CVS/* -print
というコマンドの実行結果を JAVAC_SRC_FILES に設定するということです.これが「必要なすべての java ファイル」のリストになっています.ここで find コマンドに詳しくない方のために find のコマンドライン引数を解説しましょう.
find 引数 意味 -name '*.java'*.java という名前のファイルを表す検索条件 -and検索条件を AND 条件としてつなげるための論理演算子 -not検索条件の否定を表す論理演算子 -path '*/CVS/*'CVS というサブディレクトリ以下のファイルを表す検索条件 検索結果のファイル名を標準出力に表示するコマンド
つまり,上の find コマンドが行なっていることは,
CVS ディレクトリにない *.java ファイルをすべて検索し,標準出力に表示しろ
ということです.CVS ディレクトリのファイルを除外しているのは,普段バージョン管理で CVS を使っている人のために不必要なファイルが検索されないようにするためです.
JAVAC_FIND_FLAGS マクロは,この find コマンドに条件を付加したいときに使います.例えば,コンパイルするとき *Test.java という名前のファイルを含めたくない場合,次のようにします.
JAVAC_FIND_FLAGS = -and -not -name '*Test.java' include java.mk
こうすると java.mk の find コマンドで *Test.java という名前のファイルが除外され,コンパイル対象外になります.
find コマンドは複雑でとっつきにくいですが,その機能は驚くほど多機能で便利です.詳細については man コマンドを使ってください.
$ man find
Emacs を使っている人は M-x man コマンドのほうが便利でしょう.
プロジェクト全体のテストや,あるディレクトリ以下すべてのテストを実行するのに AllTests.java というプログラムを作っている人は多いと思います.また,各テストケースを単体で動かすのにテストクラスごとに main メソッドを用意している人も多いと思います.しかし,java.mk では AllTests.java を作る必要はないし,main メソッドを用意する必要もありません.その代わりテストはすべて java-mk-lib.jar にある junit.textui.MultiTestRunner クラスで実行するようになっています.
MultiTestRunner は,複数のテストクラス名やテストメソッド名(テストクラス名.テストメソッド名で指定)を引数にとり,その引数順にテストを実行します.また, -r オプションでテストの繰り返し回数を指定することもできます.MultiTestRunner クラスのソースファイルとテストケースを用意しているので,興味のある方はご覧ください.
GNU Make のマニュアルは http://www.gnu.org/manual/make/index.html を参考にしてください.このマニュアルは make の解説としても優れています.ただし,make をあまり知らない人には難しいかもしれませんので,他の入門書籍を先に読んでおいたほうがいいでしょう.また,このマニュアルの訳本が出ていますが,翻訳の質が悪いためあまりお勧めしません.
Emacs用ライブラリの紹介を追加 ― 2003/06/15
java.mkを更新しました.次のマクロが追加されています. ― 2003/06/15
マクロ名 意味 BUILD_TEARDOWN_HOOK このマクロにターゲットを指定すると,ビルド直後にそのターゲットが実行されます. TEST_SETUP_HOOK このマクロにターゲットを指定すると,テスト直前にそのターゲットが実行されます. JAVA_TEST_FILES_FIND_FLAGS テストケースのjavaファイル検索用findコマンドオプションを追加できます.
java-mk-lib.jarで,assertEqualsのエラーメッセージのパッチを外しました. ― 2003/05/05
簡単に解決する方法がわかったからです.
ライブラリの拡張子をmakではなくmkに統一しました.― 2003/01/05
以下のようにリネームされています.
移行するには,すべてファイルを入れ替えて,MAKEFILES環境変数を/usr/local/include/config.makから/usr/local/include/config.mkにしてください.
java-mk-lib.jarがJUnit 3.8.1対応になっています. ― 2003/01/05
具体的には,expected<..> but was<..> エラーメッセージで同じものを省略せずすべて出力するようなパッチを当てています.
公開 ― 2002/08/18
[1] 保存先が /usr/local/include になっているのは,GNU Make がファイルをインクルードするサーチパスが
の順になっているためです(どうもこの設定を変えることは出来ないようです).したがって,保存先は /usr/gnu/include でも /usr/include でもかまいません.