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

seratch's weblog in Japanese

About Scala, Java and Ruby programming in Japaense. If you need English information, go to http://blog.seratch.net/

specs2 の Acceptance specification を使う

Unit specification

「org.specs2.mutable.Specification」です。specは実装コードと交互に記述されます。
単体のクラスについて記述するための利用が想定されています。

以下のような割とおなじみの書き方です。

class XXXSpec extends Specification {
  "xxx" should { 
    "yyyy" in { 
      // テストの内容
    }
  } 
}

Acceptance specification

今回の本題は「org.specs2.Specification」です。specが一つの文章のように記述され、実際のテストコードの実装は別のところに分離されます。
受け入れテストや結合テスト(シナリオテスト)での利用が想定されています。

以下のように書きます。

class XXXSpec extends Specification { def is = 
  "xxx"    ^
           p^
    "yyyy" ! example ^
           end

  def example = {
    // テストの内容
  }
}

初見だとちょっと面食らうかもしれませんが、慣れます。私自身、使うまで「取っ付きづらい・・」という印象を持っていましたが、慣れてくるとこっちの方がより spec らしいように思えます。

specs2 の準備

1.7.1 は記事時点の最新安定バージョンなので、最新の情報は specs2 のサイトで確認してください。

http://etorreborre.github.com/specs2/#Downloads

libraryDependencies += "org.specs2" %% "specs2" % "1.7.1" % "test"

specification fragments

org.specs2.Specification は is という名前のメソッドが abstract になっているので、これを実装します。

「The is method lists specification fragments」とあるように spec の「fragment(断片)」を「^」で「end」までつなげます。

def is = "title" ^ p ^ "describe1" ! example1 ^ "describe2" ! example2 ^ end

この例だとfragmentは「"title"」「p」「"describe1" ! example1」「"describe2" ! example2」「end」の5つになります。


それでは、実際にシンプルなサンプルをみてみます。

package example

import org.specs2._

class AcceptatnceSpec extends Specification { def is =

  // -------------
  // spec を記述する部分
  // 「is」というメソッドの実装として記述する 
  // 「end」キーワードまで行末を「^」でつなげていく

  "This is a spec to check 'Hello World'" ^
                                          p^  // p はparagraph(パラグラフ・段落)の p
  "The 'Hello World' string should"       ^
    "contain 11 characters"               ! isLengthValid ^ // "description" ! body の形式で書く
    "start with 'Hello'"                  ! isStartValid ^
    "end with 'World'"                    ! isEndValid ^
                                          endp^
  "The 'Hello World' string should"       ^
    "not contain numbers"                 ! notContainNumbers ^
                                          p^
  "The 'Hello World' string should"       ^
    "contain uppers"                      ! containUppers ^
                                          end

  // -------------
  // spec とは別の任意の場所にassertion の実装コードを記述する
  // これらを「example(some executable code returning a result)」と呼んでいる
  // assertion の結果 MatchResult が Result に暗黙変換され受け渡されていく

  val helloWorld = "Hello World"
  def isLengthValid = helloWorld must have size(11)
  def isStartValid =  helloWorld must startWith("Hello")
  def isEndValid = helloWorld must endWith("World")
  def notContainNumbers = helloWorld must be matching("[^0-9]+")
  def containUppers = helloWorld must =~("[A-Z]")

}

出力結果は以下のようになります。

[info] This is a spec to check 'Hello World'
[info]  
[info] The 'Hello World' string should
[info] + contain 11 characters
[info] + start with 'Hello'
[info] + end with 'World'
[info]  
[info]  
[info] The 'Hello World' string should
[info] + not contain numbers
[info]  
[info] The 'Hello World' string should
[info] + contain uppers
[info]  
[info] Total for specification AcceptatnceSpec
[info] Finished in 156 ms
[info] 5 examples, 0 failure, 0 error
「^」と組み合わせで出てくる暗号っぽいもの
  • t : タブ
  • bt : バックタブ
  • br : 空行
  • p : br ^ bt
  • endbr : end ^ br
  • endp : end ^ p
インデントを揃えるのは美意識?

特にスペースの数を揃える必要はないので、IDEのフォーマッターなども問題なく使えます。

以下はIntelliJ IDEAでフォーマットした例です。美意識の違いなどでいろいろ意見がありそうですが、、こちらでも読みづらいとは感じないと思います。

package example

import org.specs2._

class AcceptatnceSpec extends Specification {

  def is =

    "This is a spec to check 'Hello World'" ^
      p ^
      "The 'Hello World' string should" ^
      "contain 11 characters" ! isLengthValid ^
      "start with 'Hello'" ! isStartValid ^
      "end with 'World'" ! isEndValid ^
      endp ^
      "The 'Hello World' string should" ^
      "not contain numbers" ! notContainNumbers ^
      p ^
      "The 'Hello World' string should" ^
      "contain uppers" ! containUppers ^
      end

  val helloWorld = "Hello World"

  def isLengthValid = helloWorld must have size (11)

  def isStartValid = helloWorld must startWith("Hello")

  def isEndValid = helloWorld must endWith("World")

  def notContainNumbers = helloWorld must be matching ("[^0-9]+")

  def containUppers = helloWorld must =~("[A-Z]")

}