Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

JavaはBDDに近づけるか!?テストのニュージェネレーション「JDave」

まず、JDaveとは。

 JDaveは、ソフトウェア開発における「ビヘイビア駆動開発(BDD)」を促進するために開発されたJava向けのテスティングフレームワークRubyのBDD対応テスティングフレームワークrspec」から影響を受けて開発された。JDaveではモックオブジェクトフレームワークJMock 2」が統合されているほか、マッチングライブラリとしては「Hamcrest」が用いられている。

Java向けテスティングフレームワークJDave 1.0 - builder

テスト(コード)はテストではなく仕様であるという考えから「テスト」という語彙をやめて、Behavior(振る舞い)という言葉に変わったのがBDD。

RSpecのコードとかはDSL(ドメイン固有言語)で英語に近づいていて、テストコードが仕様の文章のようになる(方向に進んでいる)。

JDaveはそのJava版なんだけど、いかんせんJavaなんでDSLにはできないし、コードも文章のようにはならない。けど、なんか今までのテストコードはステートレスな感じだったし、少し見た目がよくなってると思う。

あと、マッチングライブラリ「Hamcrest」はどっかで読んだ記事でとてもいいと思った。JUnitも4.4からは「Hamcrest」使えるね。


で、JDaveの話に戻すと、使ってみるにはjarをクラスパスに通せばいい。

To use the framework you need following jars in the CLASSPATH.

* jdave-core.jar
* jdave-junit4.jar
* junit4.jar
* jmock + hamcrest + objenesis jars
* cglib-nodep.jar

JDave

とりあえずこれで動く。jmock-junit3とかjmock-legacyはもしかしたらいらないかも。

  • cglib-nodep-2.1_3.jar
  • jdave-core-1.0.jar
  • jdave-junit4-1.0.jar
  • hamcrest-core-1.1.jar
  • hamcrest-library-1.1.jar
  • jmock-2.4.0.jar
  • jmock-junit3-2.4.0.jar
  • jmock-junit4-2.4.0.jar
  • jmock-legacy-2.4.0.jar
  • objenesis-1.0.jar

テストコードはこんな感じ。JUnitと連携するのでEclipseから実行もできる。

package jyukutyo.jdave.sample;

import org.junit.runner.RunWith;

import jdave.Specification;
import jdave.junit4.JDaveRunner;

@RunWith(JDaveRunner.class)
public class CartSpec extends Specification<Cart> {

	public class EmptyCart {
		
		private Cart cart;
		
		public Cart create() {
			cart = new Cart();
			return cart;
		}
		
		public void isEmptyBeforeAddingItem() {
			specify(cart, must.be.isEmpty());
		}
		
		public void isNoLongerEmptyAfterAddingItem() {
			cart.addItem(new Item());
			specify(cart, must.not().be.isEmpty());
		}
	}

}

@RunWith(JDaveRunner.class)をテストクラスにつける。
Specificationクラスを継承する。ジェネリクスにはテストのターゲットクラスを指定する。
で、内部クラスがテストの分類みたいな感じ。
create()メソッドを定義して、ターゲットクラスのインスタンスを作る。これは規約っぽい。違うクラスを返してもコンパイルは通るから。


ここがけっこうジェネリクスのおもしろい使い方だなと思ったとこで、ジェネリクスで型を指定して、実際のインスタンスはこのcreate()メソッドの戻り値が使われる。
どういうことかというと、「be」フィールドにはcreate()メソッドの戻り値がセットされる。「be」フィールドの型はジェネリクスで指定した型になる。

public abstract class Specification<T> extends MockSupport {
    protected Specification<T> must = this;
    public T be;
}

mustはテストオブジェクト自身、beはターゲットクラスのインスタンスってこと。
specify()メソッドがassertメソッドみたいなものだ。

		public void isEmptyBeforeAddingItem() {
			specify(cart, must.be.isEmpty());
		}

コードを読むと、「cart, must.be.isEmpty()」となってる。「カートは空でなければならない」という英文ぽいよね。

ちなみにJDaveはScalaと連携するモジュールもあって、そっちはもっと英文ぽい。

§(stack, must not() be empty)


mustはテストオブジェクト自身なので、はっきり言うと書く必要自体はない。
でも、文にするために書く。ほかにshouldやdoesもあるけど。

public abstract class Specification<T> extends MockSupport {
    protected Specification<T> should = this;
    protected Specification<T> does = this;
    protected Specification<T> must = this;
}

どれもテストオブジェクト自身を表してる。つまり、文に合わせて使い分けるだけ。
あ、ちなみにmustは〜しなければならない、shouldは〜するべきだ、doesは動詞の強調で意味はないです。