我目前正在Scala中构建一个REST api,它与Mongo数据库连接。有问题的api操作会在“用户”集合中创建用户。
我正在尝试解决单元测试的问题,如果我尝试创建违反唯一键约束的记录,则数据库驱动程序会抛出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方法在手动测试下按预期工作。
答案 0 :(得分:0)
在这种情况下,您必须使用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$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)
编辑:在我们的项目中,我们定义了一组从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
- 隐含的胜利未被应用。