Scalaのユニットテスト入門
何を使うべきか?
公式サイトのForumにて
http://www.scala-lang.org/node/9826
- Java における JUnit のようなデファクトスタンダードはないの?
- ScalaTest を使うとしてどのスタイルが一般的?(JUnit、BDD、Features、FunSuite・・)
- ScalaTest、specs(specs2)、ScalaCheck、JUnit、TestNG あたりが選択肢
- どれか一つに絞るというより、ScalaTest と ScalaCheck を併用のような使い方もアリ
- 私は ScalaTest を BDD スタイルで使うことが多い
- ScalaCheck の入力を自動生成するアプローチは機能テストのようなものに特に向いている
- 私の環境(Java/Scala が混在する maven プロジェクト)には ScalaTest よりも specs の方が組み込みやすかった
- ScalaTest と specs2 はどちらも素晴らしいライブラリなのであとは好みの問題
Stack Overflowにて
- ScalaTest と specs って何が違うんですか?
- ScalaTest と specs の jar ファイルは共存できるので、どちらかだけを選択する必要はないよ
- specs は BDD のためにデザインされていて、ScalaTest はもっとテストの全般的な範囲をカバーしている
- ScalaTest の Spec は matchers を内包しないので ShouldMatchers や MustMatchers を mixin するスタイル
- specs の Specification は matchers も内包していて継承するだけで matchers も使える all-in-one
- BDD スタイルでの DSL の細かいシンタックスのポリシーが違う
- specs には優れた Mockito サポートがある
JUnit
JUnit を使ってテストを書くことができます。
JUnit によるテストは mvn test で実行できますが sbt test ではスキップされます。
import org.junit.Test import Assert._ import org.hamcrest.Matchers._ @Test class StrringTest { @Test def startsWith = { assertThat("abc".startsWith("a"), is(true)) } }
ScalaTest
ScalaTestとは?
今回取り上げる3種類のフレームワークの中で、最も様々なスタイルのテストを書くことができます。
この記事は ScalaTest 1.6.1 を対象にしています。
ScalaTest は Scaladoc が非常に充実しているので Quick Start だけでなく、そちらも参考になります。
Assertions(AssertionsForJUnit)
http://www.scalatest.org/scaladoc/1.6.1/#org.scalatest.Assertions
JUnit でテストを実行します。
import org.scalatest.Assertions import org.junit.Test class StringSuite extends Assertions { @Test def startsWith() { // ScalaTestによるassert assert("abc".startsWith("a")) // 例外発生を想定 intercept[NullPointerException] { val str: String = null str.startsWith("a") } // JUnitのAssert.*もimportすれば使える import org.junit.Assert.assertTrue assertTrue("abc".startsWith("a")) } }
ShouldMatchers
http://www.scalatest.org/scaladoc/1.6.1/#org.scalatest.matchers.ShouldMatchers
assertion に "should" を使えるようになります。
import org.scalatest.Assertions import org.scalatest.matchers.ShouldMatchers import org.junit.Test class StringShouldSuite extends Assertions with ShouldMatchers { @Test def startsWith() { "abc".startsWith("a") should be(true) } }
MustMatchers
http://www.scalatest.org/scaladoc/1.6.1/#org.scalatest.matchers.MustMatchers
assertion に "must" を使えるようになります。
import org.scalatest.Assertions import org.scalatest.matchers.MustMatchers import org.junit.Test class StringMustSuite extends Assertions with MustMatchers { @Test def startsWith() { "abc".startsWith("a") must be(true) } }
Spec
http://www.scalatest.org/getting_started_with_spec
http://www.scalatest.org/scaladoc/1.6.1/#org.scalatest.Spec
BDD スタイルのテストケースを書くことができます。
この Spec トレイトは Ruby の Rspec にインスパイアされており describe と it のネストのスタイルで記述します。
import org.scalatest.Spec import org.scalatest.matchers.ShouldMatchers class StringSpec extends Spec with ShouldMatchers { describe("abc") { // describeはネスト可能 it("should start with a") { "abc".startsWith("a") should be(true) } } }
WordSpec
http://www.scalatest.org/getting_started_with_word_spec
http://www.scalatest.org/scaladoc/1.6.1/#org.scalatest.WordSpec
WordSpec は後述の Specs にインスパイアされた BDD スタイルです。
http://code.google.com/p/specs/
import org.scalatest.WordSpec import org.scalatest.matchers.ShouldMatchers class StringWordSpec extends WordSpec with ShouldMatchers { "abc" should { "start with a" in { "abc".startsWith("a") should be(true) } } }
FlatSpec
http://www.scalatest.org/getting_started_with_flat_spec
http://www.scalatest.org/scaladoc/1.6.1/#org.scalatest.FlatSpec
FlatSpec は Spec や WordSpec のようなブロックのネストを避けた記述の BDD スタイルです。
import org.scalatest.FlatSpec import org.scalatest.matchers.ShouldMatchers class StringFlatSpec extends FlatSpec with ShouldMatchers { "abc" should "start with a" in { "abc".startsWith("a") should be(true) } }
FeatureSpec
http://www.scalatest.org/getting_started_with_feature_spec
http://www.scalatest.org/scaladoc/1.6.1/#org.scalatest.FeatureSpec
FeatureSpec は feature に対するシナリオを記述する形式の BDD スタイルです。
これまでのものと同じサンプルなので、さすがに無理やりな感じになってしまっていますが、リンク先の公式ドキュメントにはより実用的な例があります。
import org.scalatest.FeatureSpec import org.scalatest.GivenWhenThen import org.scalatest.matchers.ShouldMatchers class StringFeatureSpec extends FeatureSpec with GivenWhenThen with ShouldMatchers { feature("String value can judge it's first character.") { // sceario("startsWith works fine")(pending) scenario("startsWith works fine") { given("abc") val str = "abc" when("""when judging "abc" start with "a" """) val result = str.startsWith("a") then("the result should be true") result should be(true) } } }
FunSuite
http://www.scalatest.org/getting_started_with_fun_suite
http://www.scalatest.org/scaladoc/1.6.1/#org.scalatest.FunSuite
FunSuite はシンプルなテストケースです。
FunSuite の「Fun」は function の「fun」で、以下の「testFun」にテストを記述します。
protected def test(testName: String, testTags: org.scalatest.Tag*)(testFun : => scala.Unit): Unit
import org.scalatest.FunSuite class StringFunSuite extends FunSuite { test("abc starts with a") { assert("abc".startsWith("a")) } }
ScalaTest のテストを IntelliJ IDEA で実行しようとするとテストが実行されなかったりします。
回避方法としては、以下のように JUnitRunner を使うよう指定して IDEA から JUnit で実行すればOKです。
IDEA から実行する場面が多いプロジェクトは @RunWith(classOf[JUnitRunner]) を指定するように統一してもよいかもしれません。
import org.scalatest.FunSuite import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) class StringFunSuite extends FunSuite { test("abc starts with a") { assert("abc".startsWith("a")) } }
specs / specs2
specs とは?
http://code.google.com/p/specs/
Scala 用の BDD スタイルです。Specification を継承するだけで一通りの assertion も使うことができます。
この記事ではspecs 1.6.7 / specs2 1.5 を対象にしています。
import org.specs.Specification class StringSpecBySpecs extends Specification { "abc" should { "start with a" in { "abc".startsWith("a") mustEqual (true) } } } // Specification "StringSpecBySpecs" // abc should // + start with a // // Total for specification "StringSpecBySpecs": // Finished in 0 second, 422 ms // 1 example, 1 expectation, 0 failure, 0 error
2011/6/3 に specs2 1.0 がリリースされています。
specs もメンテナンスはされるものの新しい機能追加は specs2 にのみ行う方針とのことで、今後新しいプロジェクトを始める場合は specs ではなく specs2 を使うことを推奨されています。
http://etorreborre.github.com/specs2/
import org.specs2.mutable.Specification class StringSpecBySpecs2 extends Specification { "abc" should { "start with a" in { "abc".startsWith("a") mustEqual (true) } } }
ScalaCheck
ScalaCheck とは?
http://code.google.com/p/scalacheck/
ScalaCheck は Haskell の QuickCheck の一部として始まったもので、現在は QuickCheck にも存在しない機能もあるようです。
http://www.haskell.org/haskellwiki/Introduction_to_QuickCheck
テスト対象のふるまいを検証するコードを書くだけで、実行時にランダムで100パターンのテストが自動生成されます。
この記事は ScalaCheck 1.9 を対象にしています。
使い方
以下のような関数を用意して、forAll に渡された関数の戻り値として常に true が返るかどうかをテストする形式になります。
100パターンのパラメータによるテストが自動生成されて実行されます(=パラメータパターンは毎回異なります)。
Properties("String") や property("startsWith") で指定している文字列は、テストの実行には影響しないテスト結果の出力で使用するためのラベルです。
import org.scalacheck.Prop.forAll import org.scalacheck.Properties object StringSpecification extends Properties("String") { property("startsWith") = forAll { (a: String, b: String) => (a + b).startsWith(a) } // + String.startsWith: OK, passed 100 tests. }
テストに失敗した場合は以下のようにどういう値を渡されたかが出力されます。
以下のメッセージは、a、b ともに空文字が渡されたケースで失敗したことを表しています。
property("concat") = forAll { (a: String, b: String) => { (a.concat(b)).length > a.length && (a.concat(b)).length > b.length } } // ! String.concat: Falsified after 0 passed tests. // > ARG_0: // > ARG_1:
テストケースを書き換えると成功しました。
property("concat") = forAll { (a: String, b: String) => { // (a.concat(b)).length > a.length && (a.concat(b)).length > b.length (a.concat(b)).length >= a.length && (a.concat(b)).length >= b.length } } // + String.concat: OK, passed 100 tests.
テストは Properties 型の propety(String) として定義するだけでなく、以下のように値にすることもできます。
import org.scalacheck.Prop.forAll val propStringConcat = forAll { (a: String, b: String) => { (a + b).length >= a.length && (a + b).length >= b.length } } propStringConcat.check // + OK, passed 100 tests.
サンプルとして Int#until(Int) のテストを書いてみます。
import org.scalacheck.Prop.forAll import org.scalacheck.Properties forAll { (a: Int, b: Int) => { println("[until] a:" + a + ",b:" + b) (a until b).size == (b - a).abs || (a > b && (a until b).size == 0) } }.check
以下のようなエラーメッセージが出力されました。
... ! Exception raised on property evaluation. > ARG_0: -1440408 > ARG_1: 2146043240 > Exception: java.lang.IllegalArgumentException: -1440408 until 2146043240 by 1: seqs cannot contain more than Int.MaxValue elements. scala.collection.immutable.NumericRange$.count(NumericRange.scala:229) scala.collection.immutable.Range$.count(Range.scala:247) scala.collection.immutable.Range.numRangeElements(Range.scala:57) ...
値が Int.MaxValue を超えてしまっているようです。
これらのケースは今回のテスト対象ではないので、入力値の範囲をより小さい範囲に指定するようにします。
import org.scalacheck.Prop.forAll import org.scalacheck.Gen.choose forAll(choose(-100000,100000), choose(-100000,100000)) { (a: Int, b: Int) => { println("[until] a:" + a + ",b:" + b) (a until b).size == (b - a).abs || (a > b && (a until b).size == 0) } }.check
テストは成功しました。
... [until] a:-41559,b:54075 [until] a:-84343,b:-66921 [until] a:-12994,b:-97273 + OK, passed 100 tests.
以下のように対象のパラメータが利用者によって定義された型の場合はそのままではテストが動作しません。
case class Name(first:String, last:String) def fullName(name: Name) = name.first + " " + name.last.toUpperCase import org.scalacheck.Prop.forAll forAll { (name: Name) => fullName(name) == name.first + " " + name.last.toUpperCase }.check // error: could not find implicit value for parameter a1: org.scalacheck.Arbitrary[Name]
この Name 型の場合は、String型をテストデータ自動生成対象にするのが最も簡単ですが・・
forAll { (f: String, l: String) => fullName(Name(f,l)) == f + " " + l }.check // + OK, passed 100 tests.
エラーメッセージによると「org.scalacheck.Arbitrary[Name]」の暗黙のパラメータが必要とのことなので Name 型のテストデータ生成を定義してみます。
import org.scalacheck.Arbitrary import org.scalacheck.Gen import org.scalacheck.Gen.Params implicit val nameClass: Arbitrary[Name] = Arbitrary(Gen[Name] { (params:Params) => { val atoz = "abcdefghijklmnopqrstuvwxyz".toCharArray val rand = new java.security.SecureRandom val first = ((1 to rand.nextInt(10)) map { case i => atoz(rand.nextInt(atoz.length)) }).mkString val last = ((1 to rand.nextInt(10)) map { case i => atoz(rand.nextInt(atoz.length)) }).mkString Some(Name(first,last)) } })
この暗黙のパラメータを定義した後でテストを実行すると成功します。
forAll { (name: Name) => { println("Param: " + name) fullName(name) == name.first + " " + name.last.toUpperCase } }.check // ... // Param: Name(rhnkoof,wfpj) // Param: Name(zqvdorntl,wivs) // Param: Name(jxerptxor,iga) // + OK, passed 100 tests.
より詳細な情報はユーザガイドをご覧ください。