easyb による振舞駆動開発

easyb とは

Groovy で書かれた Java 向けの BDD(behavior driven development:振舞駆動開発)フレームワークです。Groovy の簡素な文法を利用して、BDD専用のドメイン特化言語を提供します。
その他のテスティングフレームワークでも同様のことはできますが、ソフトウェアの振舞に関する仕様を、より直感的に簡素に記述できます。本家は以下です。
http://www.easyb.org/index.html

現在のバージョンは0.9.6となっています。近々1.0がリリースされるようです。

easyb の導入

easyb のプロジェクトは Google Code でホスティングされています。本家サイトから辿って以下のファイルを落とします。
easyb-0.9.6.tar.gz
解凍すると3つのjarが含まれているのが分かります。

  • commons-cli-1.2.jar
  • easyb-0.9.6.jar
  • groovy-1.6.4.jar

今回は Eclipse 上から簡単に利用してみます。Eclipse との IDE 統合はまだあまり進んでいないため、本格的に利用するには Ant や Maven を合わせて利用する必要があります。
必須ではありませんが、Eclipse Groovy プラグインを導入しておくとエディタのハイライトが利用できるため少しだけ楽になります。導入はEclipse Groovy プラグイン - etc9を参照。

プロジェクト作成

easyb を簡単な例にて試してみます。新規 Java プロジェクトを作成し、先程解凍した3つのファイルをクラスパスに追加します。(Eclipse Groovy プラグインにて Groovy プロジェクトを作成した場合は groovy-1.6.4.jar は不要となります。)
easyb では仕様を story(拡張子.story) として記述します。この story の格納場所として stories というディレクトリを作成しておきます。

振舞(仕様)の定義

本家のチュートリアルにそって、スタックの仕様について見ていきます。スタックの振舞は以下のように定義できます。

空のスタックが与えられ、null値がプッシュされた時、例外が投げられ、スタックは空のままである。
given an empty stack, when null is pushed, an exception should be thrown and the stack should still be empty.

この仕様を easyb で記述すると以下のようになります。stories ディレクトリに MyStack.story という名前でファイルを作成します(拡張子は固定されているみたいです)。Eclipse Groovy プラグインを導入している場合は、Groovy Editor で開くとハイライト表示できます。

scenario "Pushing null", {
  given "an empty stack",{
    stack = new MyStack()
  }
  when "null is pushed", {
    pushnull = { stack.push(null) }
  }
  then "an exception should be thrown", {
    ensureThrows(RuntimeException){
        pushnull()
    }
  }
  and "then the stack should still be empty", {
    stack.empty.shouldBe true
  } 
}

given "〜", when "〜" のように英語として仕様を読みくだせるような記述ができます。日本語で書くこともできますが、ちょっと違和感が出てしまいますね。

仕様の実装

先程作成した仕様を実装していきます。まずはクラスだけ用意します。

public class MyStack<E> {
    public MyStack() { }
}

easyb を実行するには、Run Configuration.. にて Java application の新しい起動定義を作成します。Main タブにて「Include system libraries when searching for a main class」のチェックを入れ、Main Class に「org.easyb.BehaviorRunner」を指定します。
次に Arguments タブの Program arguments に「stories\MyStack.story」と作成したストーリーを指定します。Apply > Run にて作成した仕様にてテストが実施されます。
コンソールには以下が表示されるはずです。

Running my stack story (MyStack.story)
FAILURE Scenarios run: 1, Failures: 1, Pending: 0, Time elapsed: 1.218 sec
    scenario "Pushing null"
    step "then the stack should still be empty" -- No such property: empty for class: etc9.MyStack

1 behavior ran with 1 failure

empty プロパティが無いためテストが失敗しています。では仕様を満たすよう MyStack のコードを変更しましょう。以下のように isEmpty と push メソッドを追加します。

public class MyStack<E> {
    public MyStack() { }

    public boolean isEmpty(){
        return true;
    }
    public void push(E<E> value){
        throw new RuntimeException();
    }
}

再びテストを実行してみます。定義された仕様を満たすためテストは成功となります。

Running my stack story (MyStack.story)
Scenarios run: 1, Failures: 0, Pending: 0, Time elapsed: 1.219 sec

1 behavior ran with no failures

popの仕様追加

push と同じようにpopの振舞の仕様を考えます。

空のスタックが与えられ、popが呼ばれた時、例外が投げられ、スタックは空のままである。
given an empty stack, when pop is called an exception should be thrown and the stack should still be empty

これをコードに反映します。MyStack.story に以下を追加します。

scenario "Empty pops", {
  given "an empty stack",{
    stack = new MyStack()
  }
  when "pop is called", {
    popnull = { stack.pop() }
  }
  then "an exception should be thrown", {
    ensureThrows(RuntimeException){
      popnull()
    }
  }
  and "then the stack should still be empty", {
    stack.empty.shouldBe true
  }
}

MyEmpty.java に以下の pop メソッドを追加します。

public E pop(){
    throw new RuntimeException();
}

シナリオを実行して仕様を満たす動作を確認します。

push と pop の複合動作

Push と pop の複合動作時の仕様を考えます。

スタックに1つ値がプッシュされ、popが呼ばれた時、オブジェクトが返却され、スタックは空になる。
given a stack with one pushed value, when pop is called that object should be returned and the stack should be empty.

これをコードに反映します。MyStack.story に以下を追加します。

scenario "Push and pop working together", {
  given "an empty stack with one pushed value",{
    stack = new MyStack()
    pushVal = "foo"
    stack.push(pushVal)
  }
  when "pop is called", {
    popVal = stack.pop()
  }
  then "that object should be returned", {
    popVal.shouldBe pushVal
  }
  and "then the stack should be empty", {
    stack.empty.shouldBe true
  }
}

ストーリーを実行すると以下のように失敗します。

Running my stack story (MyStack.story)
FAILURE Scenarios run: 3, Failures: 1, Pending: 0, Time elapsed: 1.344 sec
    scenario "Push and pop working together"
    step "an empty stack with one pushed value" -- null
    scenario "Push and pop working together"
    step "pop is called" -- null
    scenario "Push and pop working together"
    step "that object should be returned" -- No such property: popVal for class: etc9.Script1

3 total behaviors ran with 1 failure

では振る舞いの仕様を満たす実装を行います。

public class MyStack<E> {
    private ArrayList<E> list;

    public MyStack() {
        this.list = new ArrayList<E>();
    }

    public void push(E value) {
        if (value == null) {
            throw new RuntimeException("Can't push null");
        } else {
            this.list.add(value);
        }
    }

    public boolean isEmpty() {
        return true;
    }

    public E pop() {
        if (this.list.size() > 0) {
            return this.list.remove(this.list.size() - 1);
        } else {
            throw new RuntimeException("Nothing to pop");
        }
    }
}

再度ストーリーを実行すると成功し、ここまでの仕様が実現されていることが分かります。

さらなる仕様の定義

isEmpty の実装がハードコードされたままなので、仕様を考えます。

スタックに何らかの値がpushされると、isEmpty はfalseを返却する。
given a stack with any pushed values, isEmpty should return false.

これをコードに反映します。MyStack.story に以下を追加します。

scenario "isEmpty spec", {
  given "an empty stack with one pushed value",{
    stack = new Stack()
    stack.push("bar")
  }
  then "the stack should not be empty", {
    stack.empty.shouldBe false
  }
}


複数のスタック操作に関する仕様も追加します。

スタックに2つの値があり、popが呼ばれると、最後にpushした物が得られる
given a stack with two values, when pop is called the last item pushed should be returned.

これをコードに反映します。MyStack.story に以下を追加します。

scenario "The meat of a stack", {
  given "a stack with two values",{
    stack = new MyStack()
    push1 = "foo"
    push2 = "bar"
    stack.push(push1)
    stack.push(push2)
  }
  when "pop is called", {
    popVal = stack.pop()
  }
  then "the last item pushed should be returned", {
    popVal.shouldBe push2
  }
  and "then the stack should not be empty", {
    stack.empty.shouldBe false
  }
}


MyStack.javaにisEmptyを実装して最終的には以下の実装になりました。

public class MyStack<E> {
    private ArrayList<E> list;

    public MyStack() {
        this.list = new ArrayList<E>();
    }

    public void push(E value) {
        if (value == null) {
            throw new RuntimeException("Can't push null");
        } else {
            this.list.add(value);
        }
    }

    public boolean isEmpty() {
        return this.list.isEmpty();
    }

    public E pop() {
        if (this.list.size() > 0) {
            return this.list.remove(this.list.size() - 1);
        } else {
            throw new RuntimeException("Nothing to pop");
        }
    }
}

まとめ

対象となるソフトウェアの振舞を、仕様として、ほとんどそのままの言葉で実装(シナリオ)に落とせることが見て取れたと思います。Groovy を DSL の記述に上手く使っていますね。適用箇所としては、JUnitなどの単体テストフレームワークの対象より少し粒度の大きな、サービス単位での仕様を記述することになると思います。