CLOVER🍀

That was when it all began.

ScalaTest、はじめてみます

今まで、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

どのスタイルを選択するかで、テストの書き方が変わるのですが、ここはRubyRSpecライクな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 *****

とりあえず、最低限こんなところでしょうか?あとは使って慣れていきます。