今まで、Scalaでブログを書く時はprintlnとかrequireでなんとなく書いていましたが、Javaで書く時やArquillianを使う時は突然JUnitを使い出したりと、なんかアンバランスな感じがしていたので、いい加減Scalaのテスティングフレームワークに手をつけたいと思います。
Scalaのテスティングフレームワークはいろいろありますが、さらさらと見た感じ、ScalaTestがとっつきやすそうな印象を受けたので、ScalaTestを使ってみることにします。
ScalaTest
http://www.scalatest.org/
割と基本的なフレームワークみたいなのですが、いくつかテストコードのスタイルを選ぶことができます。
Selecting testing styles
http://www.scalatest.org/user_guide/selecting_a_style
どのスタイルを選択するかで、テストの書き方が変わるのですが、ここはRubyのRSpecライクなFunSpecを選ぶことにします。
このスタイルで書いたことがないものでして。
準備
使用するScalaTestのバージョンは、現時点での最新版2.0とします。build.sbtには、以下の依存関係を追加します。
libraryDependencies += "org.scalatest" %% "scalatest" % "2.0" % "test"
テスト対象
なんでもいいのですが、今回は以下の2つのクラスを用意しました。
src/main/scala/CalcService.scala
class CalcService { def add(left: Int, right: Int): Int = left + right def multiply(left: Int, right: Int): Int = left * right }
src/main/scala/StringService.scala
class StringService { def concat(str: String, strings: String*): String = str + strings.mkString def trim(str: String): String = str.trim }
以降、これらのクラスに対してテストを書いていきます。
FunSpecを継承してassertしてみる
最初に書いた通り、今回はFunSpecを継承したスタイルで書きます。まずは、先ほどのCalcServiceクラスに対する、こんなテストを用意しました。
src/test/scala/CalcServiceTest.scala
import org.scalatest.FunSpec class CalcServiceTest extends FunSpec { describe("CalcServiceの") { describe("addのテスト") { it("1 plus 1 == 2") { val calcService = new CalcService assert(calcService.add(1, 1) === 2) } it("5 plus 5 == 10") { val calcService = new CalcService assert(calcService.add(5, 5) === 10) } } describe("multiplyのテスト") { it("1 multiply 1 == 1") { val calcService = new CalcService assert(calcService.multiply(1, 1) === 1) } it("5 multiply 5 == 25") { val calcService = new CalcService assert(calcService.multiply(5, 5) === 25) } } } }
describeとitで、テストをまとめていく感じみたいです。itが最小単位になるのかな?
この実装を選択した場合は、assertを使って確認していきます。
Using assertions
http://www.scalatest.org/user_guide/using_assertions
実行すると、こんな感じに出力されます。
> test [info] CalcServiceTest: [info] CalcServiceの [info] addのテスト [info] - 1 plus 1 == 2 [info] - 5 plus 5 == 10 [info] multiplyのテスト [info] - 1 multiply 1 == 1 [info] - 5 multiply 5 == 25
試しに、間違った結果にしてみると
assert(calcService.add(1, 1) === 3)
まあ、怒られますねと。
[info] - 1 plus 1 == 2 *** FAILED *** [info] 2 did not equal 3 (CalcServiceTest.scala:8)
Matchersを使ってみる
では、次のクラスはMatchersを使ってみましょう。
src/test/scala/StringServiceTest.scala
import org.scalatest.FunSpec import org.scalatest.Matchers._ class StringServiceTest extends FunSpec { describe("StringServiceの") { describe("concatのテスト") { it("'Hello' ' ' 'World' concat 'Hello World'") { val stringService = new StringService stringService.concat("Hello", " ", "World") should be ("Hello World") } } describe("trimのテスト") { it("' ' trim => ''") { val stringService = new StringService stringService.trim(" ") should be (empty) } } } }
Matchersオブジェクトのメンバーを、全部importします。
import org.scalatest.Matchers._
こうすると、「should」みたいな感じでテストを書いていくことができます。
stringService.concat("Hello", " ", "World") should be ("Hello World")
詳しくは、こちらのドキュメントへ…。
Using matchers
http://www.scalatest.org/user_guide/using_matchers
こちらのテストの実行結果は、こんな感じです。
> test [info] StringServiceTest: [info] StringServiceの [info] concatのテスト [info] - 'Hello' ' ' 'World' concat 'Hello World' [info] trimのテスト [info] - ' ' trim => ''
前処理、後処理を書いてみる
テスト単位の前処理、後処理を書くには、BeforeAndAfterトレイトをMix-inしてbefore、afterを書きます。
テストクラス単位の前処理、後処理を書くには、BeforeAndAfterAllトレイトをMix-inしてbeforeAll、afterAllメソッドをオーバーライドします。
両方を適用してみたクラスを、以下に記載します。
src/test/scala/BeforeAfterTest.scala
import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll, FunSpec} import org.scalatest.Matchers._ class BeforeAfterTest extends FunSpec with BeforeAndAfter with BeforeAndAfterAll { // BeforeAndAfterトレイト before { println(s"***** ${getClass} before *****") } after { println(s"***** ${getClass} after *****") } // BeforeAndAfterAllトレイト override def beforeAll(): Unit = { println(s"***** ${getClass} beforeAll *****") } override def afterAll(): Unit = { println(s"***** ${getClass} afterAll *****") } describe("String") { println("=== in describe String") describe("#toInt") { println("=== in describe #toInt") it("should be Int 10") { println("=== in should be Int 10") "10".toInt should be (10) } it("'abc' thrown NumberFormatException") { println("=== 'abc' thrown NumberFormatException") an [NumberFormatException] should be thrownBy "abc".toInt } } } }
ところどころ、printlnを入れています。
実行すると、こんな出力が得られます。
*テスト結果の表示は省略しています
> test === in describe String === in describe #toInt ***** class BeforeAfterTest beforeAll ***** ***** class BeforeAfterTest before ***** === in should be Int 10 ***** class BeforeAfterTest after ***** ***** class BeforeAfterTest before ***** === 'abc' thrown NumberFormatException ***** class BeforeAfterTest after ***** ***** class BeforeAfterTest afterAll *****
とりあえず、最低限こんなところでしょうか?あとは使って慣れていきます。