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.
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.