2

I am writing unit tests for Play application using Scalamock and Scalatest.

My original code looks like:

// Here ws is an injected WSClient
val req = Json.toJson(someRequestObject)
val resp: Future[WSResponse] = ws.url(remoteURL).post(Json.toJson(req))

In a part I have to mock external calls to a web service, which I am trying to do using scalamock:

ws = stub[WSClient]
wsReq = stub[WSRequest]
wsResp = stub[WSResponse]

ws.url _ when(*) returns wsReq
wsReq.withRequestTimeout _ when(*) returns wsReq
(wsReq.post (_: java.io.File)).when(*) returns Future(wsResp)

I am successfully able to mock post requests using a file, but I cannot mock post requests using JSON.

I tried putting stub function references separately like:

val f: StubFunction1[java.io.File, Future[WSResponse]] = wsReq.post (_: java.io.File)

val j: StubFunction1[JsValue, Future[WSResponse]] = wsReq.post(_: JsValue)

I get the compile error for second line: Unable to resolve overloaded method post

What am I missing here? Why cannot I mock one overloaded method but not the other one?

Xolve
  • 22,298
  • 21
  • 77
  • 125

4 Answers4

2

play.api.libs.ws.WSRequest has two post methods (https://www.playframework.com/documentation/2.4.x/api/scala/index.html#play.api.libs.ws.WSRequest), taking:

  1. File
  2. T (where T has an implicit bounds on Writeable)

The compiler is failing because you are trying to calling post with a single parameter, which only matches version 1. However, JsValue cannot be substituted with File.

You actually want to call the 2nd version, but this is a curried method that takes two sets of parameters (albeit the 2nd are implicit). Therefore you need to explicitly provide the mock value that you expect for the implicit, i.e.

val j: StubFunction1[JsValue, Future[WSResponse]] = wsReq.post(_: JsValue)(implicitly[Writeable[JsValue]])

Therefore a working solution would be:

(wsReq.post(_)(_)).when(*) returns Future(wsResp)

Old answer:

WSRequest provides 4 overloads of post method (https://www.playframework.com/documentation/2.5.8/api/java/play/libs/ws/WSRequest.html), taking:

  1. String
  2. JsonNode
  3. InputStream
  4. File

You can mock with a File because it matches overload 4, but JsValue does not match (this is part of the Play JSON model, whereas JsonNode is part of the Jackson JSON model). If you convert to a String or JsonNode, then it will resolve the correct overload and compile.

J0HN
  • 26,063
  • 5
  • 54
  • 85
Woodz
  • 1,029
  • 10
  • 24
  • Thanks Woodz. The question is quite specific to Scala. It doesn't have an overloaded method for JsonNode. The supposed overrride here is: `def post[T](body: T)(implicit arg0: BodyWritable[T]): Future[Response]` – Xolve Jan 31 '18 at 05:02
  • Thanks @Xolve - I missed that you were using the Scala Play API – Woodz Feb 01 '18 at 08:16
  • 2
    Thanks @J0HN. You answer provided me with what I needed for :) Though the actual solution is a little different that what it should be. 1. Explicit types are needed. 2. Two wildcards parameters are required. It would look like: `(wsReq.post(_: JsValue)(_: BodyWritable[JsValue])) when(*, *) returns Future(wsResp)` – Xolve Feb 06 '18 at 09:13
  • Also, how did you arrive at that there is an implicit involved? In the declaration of post method, there is no such thing: `override def post[T: BodyWritable](body: T): Future[Response]` – Xolve Feb 06 '18 at 09:14
  • @Xolve a context bound in Scala is syntactic sugar for an implicit parameter of that type specialised on the generic type. For example, `post[T : BodyWriteable]` is the same as `post[T](implicit evidence$0: BodyWriteable[T])` (https://docs.scala-lang.org/tutorials/FAQ/context-bounds.html) – Woodz Feb 06 '18 at 12:45
1

My best guess is that your WSRequest is actually a play.libs.ws.WSRequest which is part of the Java API, instead you should use play.api.libs.ws.WSRequest which is the Scala API.

The method WSRequest.post exists and BodyWritable[JsValue] is implicitly provided by WSBodyWritables in the Scala API but not in the Java API.

Another cause could be that your JsValue is not a play.api.libs.json.JsValue but something else (e.g. spray.json.JsValue).

Federico Pellegatta
  • 3,977
  • 1
  • 17
  • 29
1

I'll quote an example where I have successfully achieved what you are trying to do, the main difference is that I used mock instead of stub.

The important part is:

val ws = mock[WSClient]
val responseBody = "{...}"
...
"availableBooks" should {
  "retrieve available books" in {
    val expectedBooks = "BTC_DASH ETH_DASH USDT_LTC BNB_LTC".split(" ").map(Book.fromString).map(_.get).toList

    val request = mock[WSRequest]
    val response = mock[WSResponse]
    val json = Json.parse(responseBody)

    when(ws.url(anyString)).thenReturn(request)
    when(response.status).thenReturn(200)
    when(response.json).thenReturn(json)
    when(request.get()).thenReturn(Future.successful(response))

    whenReady(service.availableBooks()) { books =>
      books.size mustEqual expectedBooks.size

      books.sortBy(_.string) mustEqual expectedBooks.sortBy(_.string)
    }
  }
}

An you could see the complete test at: BinanceServiceSpec

AlexITC
  • 1,054
  • 1
  • 10
  • 21
0

I guess it should work fine, if you mock a response that is JsValue.

when(wsReq.post(Json.parse("""{...json request...}"""))).thenReturn(Future(wsResp))

Here Json.parse returns JsValue. Yo should pass the json string that you expect in the request body.

Vishal John
  • 4,231
  • 25
  • 41