Java での BDD(振舞駆動開発)- JDave と Rspec(JRuby で実行)
まず仕様を満たすための振る舞いを定義し、それに準拠するようにプログラムを段階的に開発していく手法 BDD(Behavior Driven Development, 振舞駆動開発)。
ITアーキテクト Vol.4 (IDGムックシリーズ) で知った時に、そのうち試すつもりがすっかり忘れてしまっていた。今回、エンジニアマインド Vol.8 の特集のおかげで思い出したので、とりあえず試してみる事に。
テーマは「Java の開発で BDD を実施するには」って事で以下の 2点を試してみた。
はじめ、JDave だけを試すつもりだったのだが、残念ながら JDave は工夫されているものの DSL っぽく無いし記述が面倒に感じられたので、JRuby から Rspec 使った方が実用的かと思い、こちらも試してみることに。
なお、今回は Groovy の GSpec は試さなかったが、こっちもありかなと思う。
JDave を使う(Ant からの実行)
JDave のスペックは、基本的に jdave.Specification を継承し、インナークラスを作成して、その中にメソッドとして期待する動作(振る舞い)を記述する。
なお、Apache Ant で JUnit を使って実行するには、スペッククラスに org.junit.runner.RunWith アノテーションを付与し、スペッククラスが jdave.junit4.JDaveRunner 上で実行されるようにしておき、Ant の build.xml ファイルに junit タスクを記述すればよい。
スペッククラスの作成(DataSpec.java)
スペッククラスを記述する。期待する振る舞いをインナークラスのメソッド内に記述。振る舞いは以下のような specify メソッド呼び出しで表現する。
- specify メソッドのオーバーロード(一部)
- specify(T actual, boolean expected);
- specify(Object actual, Object expected);
- specify(Object actual, Matcher matcher);
- specify(Object actual, IEqualityCheck equalityCheck);
- specify(Block block, ExpectedException
expectation); - specify(Block block, ExpectedNoThrow
expectation); - specify(Collection actual, IContainment containment);
- specify(Collection actual, Where where);
Specification クラスには should、does、must などの protected フィールドが用意されており、this オブジェクトが設定されている。これらはプログラミング上、使う必要はないが BDD 的には使った方が良いだろう。
//プログラミング上は以下でも問題ないが specify(actual.getName(), equal("aaa")); //BDD 的には以下のような記述をする specify(actual.getName(), must.equal("aaa"));
なお、specify メソッドの第2引数として渡す IEqualityCheck や ExpectedException クラス等は、Specification クラスのメソッド equal、isNotNull や raise で作成できるようになっている。
また、Specification の be や context フィールドには、インナークラスの create メソッドの戻り値が設定される。(ExecutingBehavior クラスの newContext メソッド内で設定している)
簡単なサンプルを以下のように作成した。
package sample; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasProperty; import org.junit.runner.RunWith; import jdave.Block; import jdave.Specification; import jdave.junit4.JDaveRunner; @RunWith(JDaveRunner.class) public class DataSpec extends Specification<Data> { public class BasicData { private Data data; //このメソッドの戻り値が Specification の //be と context フィールドに設定される public Data create() { data = new Data("test"); return data; } //get.getName() の戻り値が test に等しい事を定義 public void 名前を持っている() { specify(data.getName(), must.equal("test")); specify(data, must.be.getName().equals("test")); //Hamcrest を使った場合 specify(data, hasProperty("name", equalTo("test"))); } } }
実装クラスの作成(Data.java)
スペックの実装対象クラス
package sample; public class Data { private String name; public Data(String name) { this.name = name; } public String getName() { return this.name; } }
Ant ビルド定義ファイルの作成(build.xml)
Apache Ant のビルド定義ファイル。JDave のスペックを JUnit 上で実行するために junit タスクを記述。
<project name="" default="spec" basedir="."> <property environment="env" /> <property name="lib.dir" value="lib" /> <property name="src.dir" value="src" /> <property name="dest.dir" value="dest" /> <path id="project.classpath"> <pathelement path="${dest.dir}" /> <fileset dir="${env.JDAVE_HOME}/lib"> <include name="**/*.jar" /> </fileset> <fileset dir="${env.JUNIT_HOME}"> <include name="**/*.jar" /> </fileset> </path> <target name="compile"> <mkdir dir="${dest.dir}" /> <javac srcdir="${src.dir}" destdir="${dest.dir}"> <classpath refid="project.classpath" /> <include name="**/*.java" /> </javac> </target> <target name="spec" depends="compile"> <junit printsummary="yes" haltonfailure="yes"> <batchtest fork="yes"> <fileset dir="${dest.dir}"> <include name="**/*Spec.class"/> </fileset> </batchtest> <formatter type="plain" usefile="false" /> <classpath refid="project.classpath" /> </junit> </target> </project>
Ant による実行
環境変数 JUNIT_HOME に JUnit 4.4 のホームディレクトリ、JDAVE_HOME に JDave 1.0 RC1 のホームディレクトリのパスを設定し、CLASSPATH に JUnit 4.4. の junit-4.4.jar ファイルを追加した状態で ant コマンドで build.xml の spec ターゲットを実行
>set JUNIT_HOME=c:\junit4.4 >set JDAVE_HOME=c:\jdave-parent-1.0-rc1 >set CLASSPATH=.;%JUNIT_HOME%\junit-4.4.jar >ant Buildfile: build.xml compile: spec: [junit] Running sample.DataSpec [junit] Testsuite: sample.DataSpec [junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0.125 sec [junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0.125 sec [junit] [junit] Testcase: 名前を持っている took 0.062 sec BUILD SUCCESSFUL
JRuby から Rspec(Ruby用の BDD フレームワーク)を使う
JRuby 1.1 RC1 に Rspec をインストールして Java クラスの実装をチェックする事にする。
スペックファイルの作成(data_spec.rb)
describe に振る舞いの説明を記述して、it に渡されるブロック内に期待する振る舞いを記述する。振る舞いは Object クラスに対して拡張された以下のメソッドに matcher を渡すことで記述する。
- should(matcher = nil)
- should_not(matcher = nil)
matcher には以下のような演算子やビルトインを利用することができる模様。
- 演算子
- < <= == === =~ > >=
- ビルトイン(一部)
- eql(expected)
- match(regexp)
- raise_error(expected)
- respond_to(*names)
- be_true, be_false
- be_XXX(expected) 形式(XXX? メソッドの呼び出し結果を true と期待)
- have_XXX(expected) 形式(has_XXX? メソッドの呼び出し結果を true と期待)
- ユーザー定義
なお今回は、スペックの実装対象クラスは JDave の際に作成した sample.Data(Data.java)クラスをそのまま利用する事にして、簡単なスペックファイルを作成した。
require 'java' module Sample include_package "sample" end describe Sample::Data, "Data の仕様" do before do @data = Sample::Data.new("test") end it "名前を持っている" do @data.should respond_to(:getName) @data.getName().should eql("test") end it "名前は変更できない" do @data.should_not respond_to(:setName) end end