フェルマータ

個人用のメモ。ソフトウェアの導入とかが多くなる予定。

Specs2 の Mockito でデフォルト引数つきのメソッドの動作定義がメソッドの呼び出し回数にカウントされる。

経緯

 Scala でモックを使用したテストコードを書くときにに Mockito を使っている。Mockitoは大変便利で、 Specs2 で使う場合は以下のように簡単にメソッドの動作を定義できる。

val mockedHoge = mock[Hoge]
val bar: Argument = new Argument()
val result: Result = new Result()
mockedHoge.foo(bar) returns result

// Write a test with mockedHoge

there was one(mockedHoge).foo(bar)

これはHogeクラスをモックにして、Hoge#fooが引数barで呼び出されたとき、resultを返すモックを作成している。また、テスト後にmockedHoge.foo(bar)が一度だけ呼ばれていることを確認するテストである。とても簡単に書けて良い。

問題点

 しかしデフォルト引数が入ると話がややこしくなる。以下のコードは適切な引数を受け取ると、そこからエンティティを作成し expire time つきのレポジトリに登録するサービスクラスである。

  • EntityRegisterService.scala
package org.hal.stand

import scala.concurrent.Future

case class ExampleEntityId(value: Int)
case class ExampleEntity(id: ExampleEntityId)

class ExampleRepository {
  def set(entity: ExampleEntity, expiresInSecond: Int = 600): Future[Unit] = {
    Future.successful()
  }
}

class EntityRegisterService(repository: ExampleRepository) {
  def register(id: Int): Future[Unit] = {
    // 今回は引数で与えているが本来は何らかの処理で ID とエンティティを生成する。
    val entityId = ExampleEntityId(id)
    val entity = ExampleEntity(entityId)
    repository.set(entity)
  }
}

以上のサービスクラスのテストを以下のように書く。

  • EntityRegisterServiceSpec.scala
package org.hal.stand

import org.specs2.mock.Mockito
import org.specs2.mutable.Specification
import scala.concurrent._
import scala.concurrent.duration._

class EntityRegisterServiceSpec extends Specification with Mockito {
  val fakeExampleEntityId = ExampleEntityId(1234)
  val fakeEntity = ExampleEntity(fakeExampleEntityId)

  "register" should {
    "set the entity with the ID specified by the argument to the repository" in {
      val mockedRepository = mock[ExampleRepository]
      mockedRepository.set(fakeEntity) returns Future.successful()
      val service = new EntityRegisterService(mockedRepository)
      val result = service.register(fakeExampleEntityId.value)
      Await.result(result, Duration(100, MILLISECONDS)) mustEqual ()

      there was one(mockedRepository).set(fakeEntity)
    }
  }
}

一見これでうまく行きそうだがうまくいかない。以下の様なエラーが吐かれる。

[info] register should
[info] x set the entity with the ID specified by the argument to the repository
[error]    The mock was not called as expected:
[error]    exampleRepository.set$default$2();
[error]    Wanted 1 time:
[error]    -> at org.hal.stand.EntityRegisterServiceSpec$$anonfun$1$$anonfun$apply$3$$anonfun$apply$5.apply(MockitoTrapSpec.scala:20)
[error]    But was 2 times. Undesired invocation:
[error]    -> at org.hal.stand.EntityRegisterService.register(MockitoTrap.scala:19) (MockitoTrapSpec.scala:20)

どうもモックの動作定義時に一度メソッドが呼び出されたことになるらしく二回呼び出しされてエラーになってしまう。

解決方法

 デフォルト引数やめよう。

  • EntityRegisterService.scala
package org.hal.stand

import scala.concurrent.Future

case class ExampleEntityId(value: Int)
case class ExampleEntity(id: ExampleEntityId)

class ExampleRepository {
  def set(entity: ExampleEntity, expiresInSecond: Int): Future[Unit] = {
    Future.successful()
  }
}

class EntityRegisterService(repository: ExampleRepository) {
  final private val expireInSecond = 600
  def register(id: Int): Future[Unit] = {
    // 今回は引数で与えているが本来は何らかの処理で ID とエンティティを生成する。
    val entityId = ExampleEntityId(id)
    val entity = ExampleEntity(entityId)
    repository.set(entity, expireInSecond)
  }
}
  • EntityRegisterServiceSpec.scala
package org.hal.stand

import org.specs2.mock.Mockito
import org.specs2.mutable.Specification
import scala.concurrent._
import scala.concurrent.duration._

class EntityRegisterServiceSpec extends Specification with Mockito {
  val fakeExampleEntityId = ExampleEntityId(1234)
  val fakeEntity = ExampleEntity(fakeExampleEntityId)

  "register" should {
    "set the entity with the ID specified by the argument to the repository" in {
      val mockedRepository = mock[ExampleRepository]
      mockedRepository.set(fakeEntity, 600) returns Future.successful()
      val service = new EntityRegisterService(mockedRepository)
      val result = service.register(fakeExampleEntityId.value)
      Await.result(result, Duration(100, MILLISECONDS)) mustEqual ()

      there was one(mockedRepository, 600).set(fakeEntity)
    }
  }
}

今回のはサービス層で与えるべき情報がレポジトリ層でデフォルト引数で与えられていて例が大変悪いけど、呼び出し回数がうまくカウントできずに詰んだので備忘録代わりに。