/var/log/jsoizo

メモ帳 技術とか趣味とか

FutureがFailしたときの例外型を検査するユニットテストの書き方

FutureがFailureを返すときのExceptionの型を検査したい場合、どのようにUTを書けばよいかのメモ。

たとえば以下のように3の倍数または5の倍数でExceptionを吐く Foo.scala をテストしたい。

object Foo {
  
  class BarException extends Exception("bar")

  class BazException extends Exception("baz")

  def run(i: Int): Future[Unit] = if ((i % 3) == 0) {
    Future.failed(new BarException())
  } else if ((i % 5) == 0) {
    Future.failed(new BazException())
  } else Future.successful(println(s"value: $i"))

}

安直にはこのように書くことができる。

class FooSpec extends AsyncFlatSpec with RecoverMethods {

  "Foo.run" should "success when not multiple of 3" in {
    val input = 2
    Foo.run(input).map(_ => succeed)
  }

  "Foo.run" should "[old] fail when multiple of 3" in {
    val input = 3
    Foo.run(input).failed.map{t => assert(t.isInstanceOf[BarException])}
  }
}

別解としてScalaTestの RecoverMethods トレイトに recoverToSucceededIf[T] というメソッドが用意されているのでこれを使うのも良い。このメソッドは assertThrows のFuture版のようなもので、与えたFutureが例外型Tでrecoverできたら Future[Succeeded] を返却するため、まさに今回の目的に一致する。これにAsyncXXXSpecと組み合わせて使うと実装がややスッキリする。

class FooSpec extends AsyncFlatSpec with RecoverMethods {

  "Foo.run" should "fail when multiple of 3" in {
    val input = 3
    recoverToSucceededIf[BarException](Foo.run(input))
  }

  "Foo.run" should "fail when multiple of 5" in {
    val input = 5
    recoverToSucceededIf[BazException](Foo.run(input))
  }

}

Exceptionのフィールドなどを検査したい場合は recoverToExceptionIf[T] を使う。こちらはFutureが例外型Tでrecoverできたら Future[T] を返却するため、あとはmapしてassertすれば例外型の中身まで検査可能となる。

class FooSpec extends AsyncFlatSpec with RecoverMethods with Matchers {


  "Foo.run" should "fail with message \"bar\" when multiple of 3" in {
    val input = 5
    recoverToExceptionIf[BarException](Foo.run(input)).map(_.getMessage shouldBe "bar")
  }

  "Foo.run" should "fail with message \"baz\" when multiple of 5" in {
    val input = 5
    recoverToExceptionIf[BazException](Foo.run(input)).map(_.getMessage shouldBe "baz")
  }

}