seratch's weblog in Japanese

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

Scala School意訳(Testing with specs)

http://twitter.github.com/scala_school/specs.html

以下は私の方でtypoや表示崩れを直したものです。

https://github.com/seratch/scala_school/blob/master/web/_posts/2011-05-09-lesson.textile

誤訳などありましたら、お手数ですが、ご指摘いただければ幸いです。


なお、現在はspecsよりもspecs2を使うことが推奨されています。

etorreborre/specs2 @ GitHub

こちらもあわせてご覧ください。

Scalaのユニットテスト入門 - case class HatenaDiary(id: Symbol = ’seratch2)

extends Specifcation

それでは飛び込んでみましょう。

import org.specs._
object ArithmeticSpec extends Specification {
  "Arithmetic" should {
    "add two numbers" in {
      1 + 1 mustEqual 2
    }
    "add three numbers" in {
      1 + 1 + 1 mustEqual 3
    }
  }
}

Let’s break this down

"Arithmetic"はSystem under specificationを指します。

"add"はcontextです。

"add two numbers""add three numbers"はexampleです。

"mustEqual"はexpectationを示します。

"1 mustEqual 1" は実際にテストを書き始める前に使うexpectationのよくあるプレースホルダです。
全てのexampleは少なくとも一つ以上のexpetationをもたなければなりません。

Duplication

二つのテストの両方がその名前に"add"を持っていることに気づきましたか?
ネストしたexpectationを使えば、これを避けることができます。

import org.specs._
object ArithmeticSpec extends Specification {
  "Arithmetic" should {
    "add" in {
      "two numbers" in {
        1 + 1 mustEqual 2
      }
      "three numbers" in {
        1 + 1 + 1 mustEqual 3
      }
    }
  }
}

Execution Model

object ExecSpec extends Specification {
  "Mutations are isolated" should {
    var x = 0
    "x equals 1 if we set it." in {
      x = 1
      x mustEqual 1
    }
    "x is the default value if we don't change it" in {
      x mustEqual 0
    }
  }
}

Setup

doBefore & doAfter
"my system" should {
  doBefore { resetTheSystem() /** user-defined reset function */ }
  "mess up the system" in {...}
  "and again" in {...}
  doAfter { cleanThingsUp() }
}

注意:doBefore/doAfterは末端のexampleについてのみ実行されます。
doFirst & doLast
doFirst/doLastは一度きりのセットアップ処理です。
(例が必要です、私はこれを使いません)

"Foo" should {
  doFirst { openTheCurtains() }
  "test stateless methods" in {...}
  "test other stateless methods" in {...}
  doLast { closeTheCurtains() }
}

Matchers

データがあれば、それが正しいかを確認したいと思うことでしょう。
最も一般的に利用されるMatcherについてながめてみましょう。

http://code.google.com/p/specs/wiki/MatchersGuide
http://etorreborre.github.com/specs2/guide/org.specs2.guide.Matchers.html

mustEqual
これまでで既にmustEqualを使った例をいくつか見ています。

1 mustEqual 1
"a" mustEqual "a"

参照の等価と値の等価があります。
elements in a Sequence
val numbers = List(1, 2, 3)
numbers must contain(1)
numbers must not contain(4)
numbers must containAll(List(1, 2, 3))
numbers must containInOrder(List(1, 2, 3))
List(1, List(2, 3, List(4)), 5) must haveTheSameElementsAs(List(5, List(List(4), 2, 3), 1))
Items in a Map
map must haveKey(k)
map must notHaveKey(k)
map must haveValue(v)
map must notHaveValue(v)
Numbers
a must beGreaterThan(b)
a must beGreaterThanOrEqualTo(b)
a must beLessThan(b)
a must beLessThanOrEqualTo(b)
a must beCloseTo(b, delta)
Options
a must beNone
a must beSome[Type]
a must beSomething
a must beSome(value)
throwA
a must throwA[WhateverException]

これは(例外発生を確認するテストを書くときに)try catchとfailを使ってテストのボディの中で書くよりも短く済ませることができます。
固有のメッセージも指定することができます。

a must throwA(WhateverException("message"))

また期待値とマッチさせる事もできます。

a must throwA(new Exception) like {
  case Exception(m) => m.startsWith("bad")
}
Write your own Matchers
valとして・・

import org.specs.matcher.Matcher
"A matcher" should {
  "be created as a val" in {
    val beEven = new Matcher[Int] {
      def apply(n: => Int) = {
        (n % 2 == 0, "%d is even".format(n), "%d is odd".format(n))
      }
    }
    2 must beEven
  }
}

ここでの規約はexpectationがtrueだったかとそれがtrue出なかった場合のメッセージを含むタプルを返すことです。

case classとして・・

case class beEven(b: Int) extends Matcher[Int]() {
  def apply(n: => Int) =  (n % 2 == 0, "%d is even".format(n), "%d is odd".format(n))
}

case classを使うとより再利用可能になります。
Mocks
import org.specs.Specification
import org.specs.mock.Mockito
class Foo[T] {
  def get(i: Int): T
}

object MockExampleSpec extends Specification with Mockito {
  val m = mock[Foo[String]]
  m.get(0) returns "one"
  m.get(0)
  there was one(m).get(0)
  there was no(m).get(1)
}
Spies
spyは実際のオブジェクトを"部分的にモックにする"という用途に使われます。

val list = new LinkedList[String]
val spiedList = spy(list)

// メソッドはspyによってスタブ化される
spiedList.size returns 100

// その他のメソッドも使われる
spiedList.add("one")
spiedList.add("two")

// そして、検証がspyによって行われる
there was one(spiedList).add("one")

しかし、spyを使うとトリッキーなことになる場合があります。

// もしリストが空だったらIndexOutOfBoundsExceptionがthrowされる
spiedList.get(0) returns "one"
doReturn must be used in that case:
doReturn("one").when(spiedList).get(0)
Run individual specs in sbt
> test-only com.twitter.yourservice.UserSpec

指定されたspecだけが実行されます。

> ~ test-only com.twitter.yourservice.UserSpec

ループの中でファイルの変更をトリガーにしてテストが実行されます。

specs2を使おう

ここまでの例を可能な限りそのままでspecs2を使ったものにしてみました。