Magnolia Tech

いつもコードのことばかり考えている人のために。

Specs2でテストに失敗したときに例外を投げる

Scala用の代表的なテスティングフレームワークの1つがSpecs2です。

"Acceptance specification"と呼ばれる自由度の高い仕様の記述方法が特徴的です。

以下のコードはDeepThrought. calcUltimateQuestionメソッドの結果の正当性を確認するテストコードです。

import org.specs2._

object DeepThrought {
  def calcUltimateQuestion: Int = {
    // Because this calculation requires strictly 7.5 million years,
    // I will write the answer first :)
    42
  }
}

class UltimateAnswerSpec extends Specification { def is = s2"""
the Answer to the Ultimate Question of Life, the Universe, and Everything $checkCalc
"""

  def checkCalc = {
    val answer = DeepThrought.calcUltimateQuestion
    answer must be_==(42)
  }
}

def is = s2"""..."""で囲まれた中に仕様を自由に記述し、テストを実行するメソッドのメソッド名を$を付けて埋め込むと、テストが実行された時に結果が埋め込まれ、成否が表示されます(上記の例でいえば、caclメソッド)。

> test
...
[info] Done compiling.
[info] UltimateAnswerSpec+ the Answer to the Ultimate Question of Life, the Universe, and Everything
[info] Total for specification UltimateAnswerSpec
[info] Finished in 64 ms
[info] 1 example, 0 failure, 0 error
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 2 s, completed Dec 5, 2017 9:41:10 PM

テストクラスの名称の後ろに「+」が表示されていれば、そのテストクラスのテストは成功したことになります。

試しにテストコードの数値を「43」に書き換えると、当然演算結果が一致しないのでテストは失敗します。

[error] x the Answer to the Ultimate Question of Life, the Universe, and Everything
[error]  42 != 43 (HelloSpec.scala:17)
[info] UltimateAnswerSpec

実際のテストの判定は、マッチャーと呼ばれるメソッドで行います。上記の例では、テストメソッドの最後にある「answer must be_==(42)」がマッチャーを使った箇所になります。このコードでは、計算結果が格納されたanswerという変数が42でなければならないことを表しています。

判定結果はそのままメソッドの返り値として使われ、テストの成否が判定されます(Scalaではメソッドの最後に書かれた値が返り値になります)。

ここまでは通常のspecs2の使い方です。

よくある(?)ミス

これは自分だけかもしれませんが、specs2を初めて使ったとき、あと久しぶりに使った時によくやる間違いが、テストメソッドの中でマッチャーを複数使うことです。

極端な例ですが、下記のテストは、テストメソッド全体としては成功してしまいます。

answer must be_>(43)
answer must be_==(42)

なぜならば、あくまでspecs2ではテストメソッドの戻り値だけがテストの成否に使われるからです。

PerlのTest::Moreなどに慣れていると、うっかり書いてしまいますね。

対応策1: andで連結する

こんな時はマッチャーの結果をandで連結します(上記の例を成功するように書き換えています)。

answer must be_<(43) and be_>(41)

テスト対象の変数が複数有るときはコードブロックで連結します。

    {
      question must be_==("How many roads must a man walk down?")
    } and {
      answer must be_==(42)
    }

対応策2: ThrownExpectations traitを使う

ThrownExpectations traitを使うと、マッチャーが失敗すると、例外を送出し、テストが失敗します。

class UltimateAnswerSpec extends Specification with org.specs2.matcher.ThrownExpectations { def is = s2"""
the Answer to the Ultimate Question of Life, the Universe, and Everything $checkCalc
"""

  def checkCalc = {
    val answer = DeepThrought.calcUltimateQuestion

    answer must be_>(43)
    answer must be_==(42)
  }
}

結果はこうなります。

[error] x the Answer to the Ultimate Question of Life, the Universe, and Everything
[error]  42 is less than 43 (UltimateAnswerSpec.scala:22)

おわりに

うっかり書いてしまうテストメソッド内の複数マッチャーに対する対策を書きました。

ThrownExpectationsは、なんとspecs2の作者で有る@etorreborre‏さんに教えてもらいました。感謝!

基礎からわかる Scala

Scalaスケーラブルプログラミング第3版