Blogaomu

WEBアプリケーション開発とその周辺のメモをゆるふわに書いていきます。

ScalaTest の OptionValues が良さげ

ScalaTestを使っていて便利な機能があったので小ネタとして紹介してみます。

OptionValues

Option 型の値を検証したいときに、 OptionValues を使うとOption値が定義されているかとその値についての検証を同時に行うことができます。

http://www.scalatest.org/user_guide/using_OptionValues

// やりがちな例
opt.get should be > 9 // opt が Noneの場合 NoSuchElementException が起きてテストが失敗する(後述)

// より安全な方法
opt should be ('defined) // opt が None の場合 TestFailedException が発生。どこで失敗したかの情報が得られる
opt.get should be > 9

// OptionValuesを使う
import org.scalatest.OptionValues._

opt.value should be > 9 // Option型の値に対して `value` メソッドを使えるようになる
// opt が None の場合 TestFailedException が発生

// テストクラスに対しては OptionValues trait を mix-in するとよい
class ExampleSpec extends WordSpec with Matchers with OptionValues {
  // この中のテストコードで `value` メソッドを使えるようになる
}

実際やってみるとこんな感じです。

import org.scalatest.{Matchers, OptionValues, WordSpec}

class ExampleSpec extends WordSpec with Matchers with OptionValues {

  "Option value is defined" in {
    val opt: Option[Int] = Some(10)
    opt.value should be > 9
  }

  "Option value is defined but is not match condition" in {
    val opt: Option[Int] = Some(1)
    opt.value should be > 9
  }

  "Option value is not defined" in {
    val opt: Option[Int] = None
    opt.value should be > 9
  }

  "Option value is not defined and access with get" in {
    val opt: Option[Int] = None
    opt.get should be > 9
  }
}

以下はテスト実行時の出力。注目してほしい点は3番目と4番目のテストの違いで、 value を使った場合はテストの失敗内容が簡潔にまとめられて出力されていますが、 get を使った場合は通常の例外における出力(Exceptionのメッセージ + スタックトレース)となっています。

[info] ExampleSpec:
[info] - Option value is defined
[info] - Option value is defined but is not match condition *** FAILED ***
[info]   1 was not greater than 9 (ExampleSpec.scala:12)
[info] - Option value is not defined *** FAILED ***
[info]   The Option on which value was invoked was not defined. (ExampleSpec.scala:17)
[info] - Option value is not defined and access with get *** FAILED ***
[info]   java.util.NoSuchElementException: None.get
[info]   at scala.None$.get(Option.scala:349)
[info]   at scala.None$.get(Option.scala:347)
[info]   at ExampleSpec.$anonfun$new$4(ExampleSpec.scala:22)
[info]   at ExampleSpec$$Lambda$5658/224979600.apply(Unknown Source)
[info]   at org.scalatest.OutcomeOf.outcomeOf(OutcomeOf.scala:85)
[info]   at org.scalatest.OutcomeOf.outcomeOf$(OutcomeOf.scala:83)
[info]   at org.scalatest.OutcomeOf$.outcomeOf(OutcomeOf.scala:104)
[info]   at org.scalatest.Transformer.apply(Transformer.scala:22)
[info]   at org.scalatest.Transformer.apply(Transformer.scala:20)
[info]   at org.scalatest.WordSpecLike$$anon$1.apply(WordSpecLike.scala:1078)
[info]   ...
[info] ScalaTest
[info] Run completed in 436 milliseconds.
[info] Total number of tests run: 4
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 3, canceled 0, ignored 0, pending 0
[info] *** 3 TESTS FAILED ***
[error] Failed: Total 4, Failed 3, Errors 0, Passed 1
[error] Failed tests:
[error]     ExampleSpec
[error] (Test / testOnly) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 2 s, completed 2018/08/26 11:35:09

EitherValues, TryValues, PartialFunctionValues

OptionValues と同じような働きをするtraitは他にもあって EitherValues TryValues PartialFunctionValues というものがあります。これも中の値の定義と検証を同時に行い、失敗した場合は TestFailedException を起こしてくれます。

http://www.scalatest.org/user_guide/using_EitherValues

http://www.scalatest.org/user_guide/using_PartialFunctionValues

(なぜかTryValuesについてのページは存在しない😅)

// TryValues
import org.scalatest.TryValues._

val try1: Try[Int] = Try { 100 / 10 }
val try2: Try[Int] = Try { 1 / 0 }
// enable `success` and `failure`
try1.success.value should be > 9
try2.failure.exception should have message "/ by zero"

// EitherValues
import org.scalatest.EitherValues._

val either1: Either[String, Int] = Right(10)
val either2: Either[String, Int] = Left("Muchas problems")
// enable `value`
either1.right.value should be > 9
either2.left.value should be ("Muchas problemas")

// PartialFunctionValues
import org.scalatest.PartialFunctionValues._

val pf: PartialFunction[String, Int] = Map("I" -> 1, "II" -> 2, "III" -> 3, "IV" -> 4)
// enable `valueAt`
pf.valueAt("IV") should equal (4)