Search code examples
mongodbscalaunit-testingmockitoreactivemongo

Mocking ReactiveMongo checked exceptions with Mockito


I'm currently building a REST api in Scala which interfaces with a Mongo database. The api action in question creates users in a "users" collection.

I'm trying to cover an issue with a unit test where the database driver throws a DatabaseException if I attempt to create a record which violates a unique key constraint. Using Mockito, I have this so far:

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")
    }
  }

This is the api method under test:

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)
}

The error I'm getting when running the tests is this: Checked exception is invalid for this method and, from my limited knowledge of Scala, Java and how exception handling works, I understand that methods have to declare the exceptions they expect to throw, which is why this error might occur.

How can I move forward from here and test this scenario? For what it's worth, the api method works as expected under manual testing.


Solution

  • You'll have to resort to using Answer in this case. Here's an example from 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$1.answer(/dummy.sc:14)
    //     at #worksheet#.$anon$1.answer(/dummy.sc:13)
    //     at org.mockito.internal.stubbing.StubbedInvocationMatcher.answer(/dummy.sc:30)
    //     at #worksheet#.#worksheet#(/dummy.sc:87)
    

    Edit: In our project we defined a set of implicit conversions from FunctionN to Answer so there's less boilerplate in such cases, like the following:

    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.")
      }
    }
    

    Edit 2: As for working with Futures in Mockito, considering their almost core language feature semantics, here's another very convenient wrapper I invented to simplify unit-testing:

    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 is straightforward and transparent against the original method (even allows you to convert existing synchronous code to asynchronous with less fixes in tests). thenFail is a little less so, but we are unable to define thenThrow for this case - the implicit won't be applied.