使用 Mockito 模拟 ReactiveMongo 检查异常

Mocking ReactiveMongo checked exceptions with Mockito

我目前正在 Scala 中构建一个 REST api,它与 Mongo 数据库交互。有问题的 api 操作在 "users" 集合中创建用户。

我试图解决一个单元测试问题,如果我尝试创建违反唯一键约束的记录,数据库驱动程序将抛出 DatabaseException。使用 Mockito,到目前为止我有这个:

describe("a mongo db error") {

    val collection = mockCollection(Some("users"))

    doThrow(GenericDatabaseException("Test exception", None))
      .when(collection)
      .insert(any(), any())(any(), any())

    val userRequest = CreateUserRequest("test", "test", "test")
    val request = FakeRequest().withJsonBody(Json.toJson(userRequest))
    val result = call(controller.post, request)
    val response = Json.fromJson[GenericResponse](contentAsJson(result)).get

    it("should return a bad request") {
      response.status must be("Failed")
    }
  }

这是正在测试的 api 方法:

def post = Action.async(parse.json) { implicit request =>
request.body.validate[CreateUserRequest].map {
  case model => {
    collection flatMap { c =>

      val hashedPassword = SecureHash.createHash(model.password)

      c.insert(User(model.username, hashedPassword, model.emailAddress)) flatMap { r =>
        c.indexesManager.ensure(Index(List(("username", IndexType.Ascending)), unique = true)) map { r =>
          Ok
        }
      } recover {
          case dex: DatabaseException => BadRequest(Json.toJson(GenericResponse("Failed")))

      }
    }

  }
}.recoverTotal { e =>

  val errorResponse = BadRequest(Json.obj(
    "status" -> Messages("status.invalid"),
    "message" -> Messages("error.generic.invalid_request")))

  Future.successful(errorResponse)
}

我在 运行 测试时遇到的错误是这样的:Checked exception is invalid for this method 而且,根据我对 Scala 的有限了解,Java 以及异常处理的工作原理,我明白方法必须声明它们希望抛出的异常,这就是为什么会出现此错误的原因。

我怎样才能从这里继续前进并测试这个场景?对于它的价值,api 方法在手动测试下按预期工作。

在这种情况下,您将不得不使用 Answer。 这是来自 REPL 的示例:

import org.mockito.Matchers.{eq => exact, _}
import org.mockito.Mockito._
import org.mockito.invocation.InvocationOnMock
import org.mockito.stubbing.Answer
import org.scalatest.mock.MockitoSugar

trait MyService {
  def insert(v: String): String
}

val mk = MockitoSugar.mock[MyService]

when(mk.insert(any())).thenAnswer(new Answer[String] {
  def answer(invocation: InvocationOnMock): String =
    throw new Exception("this should have never happened")
})

mk.insert("test")
// java.lang.Exception: this should have never happened
//     at #worksheet#.$anon.answer(/dummy.sc:14)
//     at #worksheet#.$anon.answer(/dummy.sc:13)
//     at org.mockito.internal.stubbing.StubbedInvocationMatcher.answer(/dummy.sc:30)
//     at #worksheet#.#worksheet#(/dummy.sc:87)

编辑:在我们的项目中,我们定义了一组从 FunctionN 到 Answer 的隐式转换,因此在这种情况下样板文件更少,如下所示:

implicit def function1ToAnswer[T, R](function: T => R)(implicit ct: ClassTag[T]): Answer[R] = new Answer[R] {
  def answer(invocation: InvocationOnMock): R = invocation.getArguments match {
    case Array(t: T, _*) => function(t)
    case arr => fail(s"Illegal stubbing, first element of array ${arr.mkString("[", ",", "]")} is of invalid type.")
  }
}

编辑 2:至于在 Mockito 中使用 Futures,考虑到它们几乎是核心语言特性语义,这是我发明的另一个非常方便的包装器来简化单元测试:

implicit class ongoingStubbingWrapperForOngoingStubbingFuture[T](stubbing: OngoingStubbing[Future[T]]) {
  def thenReturn(futureValue: T): OngoingStubbing[Future[T]] = stubbing.thenReturn(Future.successful(futureValue))
  def thenFail(throwable: Throwable): OngoingStubbing[Future[T]] = stubbing.thenReturn(Future.failed(throwable))
}

thenReturn 相对于原始方法是直接和透明的(甚至允许您将现有的同步代码转换为异步代码,而在测试中修复较少)。 thenFail 稍微少一点,但我们无法为这种情况定义 thenThrow - 隐式将不会应用。