読者です 読者をやめる 読者になる 読者になる

Maven での BDD - Specs, Specs2, RSpec, Easyb, spock

Java BDD Ruby Scala Groovy

Maven3 を使ったプロジェクトでの BDD(振舞駆動開発)の実施方法をまとめてみました。
今回試した BDD ツールは以下の通りです。

結果として、この中では Specs/Specs2 か spock あたりを使うのが良さそうです。(RSpec と Easyb は問題あり)

なお、実行環境は以下の通りです。

サンプルのソースは http://github.com/fits/try_samples/tree/master/blog/20110321/

Specs の場合

Scala による BDD ツールの Specs を使用する場合、pom.xml を以下のように設定します。
なお、surefire プラグインのデフォルト設定では XXXSpec クラスは実行対象にならないため、include で **/*Spec.java を追加しています。(実ファイルの拡張子は .scala ですが、拡張子を .java で指定する点に注意)

pom.xml
<project ・・・>
  ・・・
  <properties>
    <scala.version>2.8.1</scala.version>
  </properties>
  <!-- Specs 等 Scala 関連ライブラリを取得するためのリポジトリ設定 -->
  <repositories>
    <repository>
      <id>scala-tools.org</id>
      <name>releases</name>
      <url>http://scala-tools.org/repo-releases</url>
    </repository>
  </repositories>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <includes>
            <!--
              XXXSpec クラスを実行対象にするための設定
              拡張子を .java で include しなければならない点に注意
             -->
            <include>**/*Spec.java</include>
          </includes>
        </configuration>
      </plugin>
      <!-- Scala によるスペックファイルをテスト時にコンパイルするための設定 -->
      <plugin>
        <groupId>org.scala-tools</groupId>
        <artifactId>maven-scala-plugin</artifactId>
        <executions>
          <execution>
            <goals>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <scalaVersion>${scala.version}</scalaVersion>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <!-- junit の定義が必要 -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.8.2</version>
      <scope>test</scope>
    </dependency>
    <!-- Specs の設定 -->
    <dependency>
      <groupId>org.scala-tools.testing</groupId>
      <artifactId>specs_${scala.version}</artifactId>
      <version>1.6.7.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

次に、スペックファイルは以下のようになります。src/test/scala に配置します。
他にも org.specs.runner.JUnit4 を使って定義する方法等がありますが、今回はシンプルに定義できる方法を採用しています。

src/test/scala/BookSpec.scala
package fits.sample

import scala.collection.JavaConversions._
import org.specs._

class BookSpec extends SpecificationWithJUnit {

    "初期状態" should {
        val b = new Book()

        "comments は null ではない" in {
            b.getComments() must notBeNull
        }

        "comments は空" in {
            b.getComments() must haveSize(0)
        }
    }

    "Comment を追加" should {
        val b = new Book()
        b.getComments().add(new Comment())

        "Comment が追加されている" in {
            b.getComments() must haveSize(1)
        }
    }
}

mvn test で実行します。

実行例
> mvn test
・・・
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running fits.sample.BookSpec
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.483 sec

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0

Specs2 の場合

Specs の次期バージョン Specs2 は以下のようになります。基本的に Specs と同様ですが、groupId やパッケージ名、not の使い方に違いがあります。

pom.xml
<project ・・・>
  ・・・
  <properties>
    <scala.version>2.8.1</scala.version>
  </properties>
  <repositories>
    <repository>
      <id>scala-tools.org</id>
      <name>releases</name>
      <url>http://scala-tools.org/repo-releases</url>
    </repository>
  </repositories>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <includes>
            <include>**/*Spec.java</include>
          </includes>
        </configuration>
      </plugin>
      <!-- Scala によるスペックファイルをテスト時にコンパイルするための設定 -->
      <plugin>
        <groupId>org.scala-tools</groupId>
        <artifactId>maven-scala-plugin</artifactId>
        <executions>
          <execution>
            <goals>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <scalaVersion>${scala.version}</scalaVersion>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <!-- Specs2 の設定 -->
    <dependency>
      <groupId>org.specs2</groupId>
      <artifactId>specs2_${scala.version}</artifactId>
      <version>1.0.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
src/test/scala/BookSpec.scala
package fits.sample

import scala.collection.JavaConversions._
import org.specs2.mutable._

class BookSpec extends SpecificationWithJUnit {

    "初期状態" should {
        val b = new Book()

        "comments は null ではない" in {
            b.getComments() must not beNull
        }

        "comments は空" in {
            b.getComments() must haveSize(0)
        }
    }

    "Comment を追加" should {
        val b = new Book()
        b.getComments().add(new Comment())

        "Comment が追加されている" in {
            b.getComments() must haveSize(1)
        }
    }
}

Specs と同様に mvn test で実行します。

実行例
> mvn test
・・・
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running fits.sample.BookSpec
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.546 sec

Results :

Tests run: 5, Failures: 0, Errors: 0, Skipped: 0

なぜかテスト数のカウントが Specs と異なっているようです。

RSpec の場合

Ruby の BDD ツール RSpecMaven で使うには一手間かかりました。(実用的では無いかもしれません)
今回は de.saumya.mojo の rspec-maven-plugin を使っていますが、別のプラグインを使う方法もあるようです。

pom.xml では TorqueBox RubyGems Maven Proxy Repository を使って RSpec の gem を Maven から取得できるようにしました。(RubyGems を直接リポジトリに設定する方法は駄目でした)
また、properties 要素内の jruby.version 要素で実行する JRuby のバージョンを指定できます。

pom.xml
<project ・・・>
  ・・・
  <properties>
    <!-- JRuby のバージョン指定-->
    <jruby.version>1.6.0</jruby.version>
    <jruby.plugins.version>0.25.1</jruby.plugins.version>
  </properties>

  <repositories>
    <!-- TorqueBox RubyGems Maven Proxy Repository -->
    <repository>
      <id>rubygems-proxy</id>
      <name>Rubygems Proxy</name>
      <url>http://rubygems-proxy.torquebox.org/releases</url>
      <layout>default</layout>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>fale</enabled>
        <updatePolicy>never</updatePolicy>
      </snapshots>
    </repository>
  </repositories>
  <build>
    <plugins>
      <!-- rspec-maven-plugin の設定 -->
      <plugin>
        <groupId>de.saumya.mojo</groupId>
        <artifactId>rspec-maven-plugin</artifactId>
        <version>${jruby.plugins.version}</version>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <!-- RSpec の設定 -->
    <dependency>
      <groupId>rubygems</groupId>
      <artifactId>rspec</artifactId>
      <version>2.5.0</version>
      <type>gem</type>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

スペックファイルは以下のようになります。spec に配置します。

spec/book_spec.rb
require "java"

module Fits
    include_package "fits.sample"
end

describe "Book" do
    context "初期状態" do
        before do
            @b = Fits::Book.new
        end

        it "comments は nil ではない" do
            @b.comments.should_not be_nil
        end

        it "comments は空" do
            @b.comments.size.should == 0
        end
    end

    context "Comment を追加" do
        before do
            @b = Fits::Book.new
            @b.comments.add(Fits::Comment.new)
        end

        it "Comment が追加されている" do
            @b.comments.size.should == 1
        end
    end
end

mvn rspec:test で RSpec が実行されますが、実はこのままでは下記のようなエラーが出て失敗します。

実行例(対策前)
> mvn rspec:test
・・・
[INFO] Running RSpec tests from ・・・\20110321\rspec\spec
[WARNING] NameError: uninitialized constant RSpec::Core::Formatters::BaseFormatter::StringIO
・・・
[WARNING]            load at org/jruby/RubyKernel.java:1062
[WARNING]          (root) at -e:1
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.308s
[INFO] Finished at: Mon Mar 21 16:46:28 JST 2011
[INFO] Final Memory: 3M/15M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal de.saumya.mojo:rspec-maven-plugin:0.25.1:test (default-cli) on project maven-rspec-sample1: Execution default-cli of goal de.saumya.mojo:rspec-maven-plugin:0.25.1:test failed: Java returned: 1 -> [Help 1]
・・・

これを回避するには rspec:test の実行時に自動生成された target/rspec-runner.rb の 48 行目をコメントアウト化して、ファイルを読み取り専用にしてしまいます。

target/rspec-runner.rb の48行目をコメントアウト化
::RSpec.configure do |config|
 # config.formatter = ::MultiFormatter
end

とりあえずこれで、ビルドは失敗扱いになるものの、一応 RSpec は実行されるようになります。

実行例(対策後)
> mvn rspec:test
・・・
[ERROR] error emitting .rb
java.io.FileNotFoundException: ・・・\rspec\target\rspec-runner.rb (アクセスが拒否されました。)
・・・
[INFO] ...
[INFO]
[INFO] Finished in 0.016 seconds
[INFO] 3 examples, 0 failures
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
・・・

上記のように rspec-runner.rb の内容を書き換える方法で回避する場合、rspec-maven-plugin の de.saumya.mojo.rspec.RSpec2ScriptFactory クラスの getRSpecRunnerScript メソッドの戻り値を AspectJ とかで書き換えてやればもう少しマシになると思います。

Easyb の場合

Groovy による BDD ツールの Easyb です。こちらは Maven 3.0.3 で実行するとエラーが出るため(http://code.google.com/p/easyb/issues/detail?id=209)、Maven 2.2.1 で実行する事にします。
pom.xml ファイルは以下のようになります。

pom.xmlMaven 2.2.1 用)
<project ・・・>
  ・・・
  <build>
    <plugins>
      <!-- Maven 2.2.1 で JavaSE 6 のソースを使うための設定 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
      <!-- Easyb の設定 -->
      <plugin>
        <groupId>org.easyb</groupId>
        <artifactId>maven-easyb-plugin</artifactId>
        <version>0.9.7-1</version>
        <executions>
          <execution>
            <goals>
              <goal>test</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

スペックファイルは以下の通りです。src/test/easyb に配置します。
なお、日本語を使用すると実行時にエラーが発生する点に注意。

src/test/easyb/BookStory.groovy
package fits.sample

scenario "init state", {
    given "Book", {
        b = new Book()
    }
    when ""
    then "comments is not null", {
        b.comments.shouldNotBe null
    }
    and
    then "comments is empty", {
        b.comments.size.shouldBe 0
    }
}

scenario "add Comment", {
    given "Book", {
        b = new Book()
    }
    when "add Comment", {
        b.comments.add(new Comment())
    }
    then "added Comment", {
        b.comments.size.shouldBe 1
    }
}

mvn test で実行します。

実行例(Maven 2.2.1 で実行)
> mvn test
・・・
[INFO] Using easyb dependency org.easyb:easyb:jar:0.9.7:compile
[INFO] Using easyb dependency commons-cli:commons-cli:jar:1.1:compile
[INFO] Using easyb dependency org.codehaus.groovy:groovy-all:jar:1.7.2:compile
     [java] Running book story (BookStory.groovy)
     [java] Scenarios run: 2, Failures: 0, Pending: 0, Time elapsed: 0.577 sec
     [java] 2 total behaviors ran with no failures
・・・

Spock の場合

最後に Groovy による BDD ツール spock です。
pom.xml ファイルは以下のようになります。(surefire を設定する点は Specs と同様)

pom.xml
<project ・・・>
  ・・・
  <build>
    <plugins>
      <!-- XXXSpec を実行対象にするための設定 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <includes>
            <include>**/*Spec.java</include>
          </includes>
        </configuration>
      </plugin>
      <!-- Groovy によるスペックファイルをテスト時にコンパイルするための設定 -->
      <plugin>
        <groupId>org.codehaus.gmaven</groupId>
        <artifactId>gmaven-plugin</artifactId>
        <version>1.3</version>
        <configuration>
          <providerSelection>1.7</providerSelection>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <!-- spock の設定 -->
    <dependency>
      <groupId>org.spockframework</groupId>
      <artifactId>spock-core</artifactId>
      <version>0.5-groovy-1.7</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

スペックファイルは以下の通りです。src/test/groovy に配置します。

src/test/groovy/BookSpec.groovy
package fits.sample

import spock.lang.*

class InitBookSpec extends Specification {
    def b = new Book()

    def "comments は null ではない"() {
        expect:
            b.comments != null
    }

    def "comments は空"() {
        expect:
            b.comments.size == 0
    }
}

class AddCommentSpec extends Specification {
    def b = new Book()

    def "Comment を追加"() {
        when:
            b.comments.add(new Comment())
        then:
            b.comments.size == 1
    }
}

mvn test で実行します。

実行例
> mvn test
・・・
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running fits.sample.AddCommentSpec
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.468 sec
Running fits.sample.InitBookSpec
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.015 sec

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0