Skip to content

Commit

Permalink
Merge pull request #61 from softwaremill/then-respond-future
Browse files Browse the repository at this point in the history
Allow SttpBackendStub to accept monad with response
  • Loading branch information
adamw committed Jan 29, 2018
2 parents b5e8aa3 + 2f2c6b6 commit 88a3a82
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import scala.util.{Failure, Success, Try}
*/
class SttpBackendStub[R[_], S] private (
rm: MonadError[R],
matchers: PartialFunction[Request[_, _], Response[_]],
matchers: PartialFunction[Request[_, _], R[Response[_]]],
fallback: Option[SttpBackend[R, S]])
extends SttpBackend[R, S] {

Expand Down Expand Up @@ -60,13 +60,14 @@ class SttpBackendStub[R[_], S] private (
def whenRequestMatchesPartial(
partial: PartialFunction[Request[_, _], Response[_]])
: SttpBackendStub[R, S] = {
new SttpBackendStub(rm, matchers.orElse(partial), fallback)
val wrappedPartial = partial.andThen(rm.unit)
new SttpBackendStub(rm, matchers.orElse(wrappedPartial), fallback)
}

override def send[T](request: Request[T, S]): R[Response[T]] = {
Try(matchers.lift(request)) match {
case Success(Some(response)) =>
wrapResponse(tryAdjustResponseType(request.response, response))
case Success(Some(responseMonad)) =>
tryAdjustResponseType(rm, request.response, wrapResponse(responseMonad))
case Success(None) =>
fallback match {
case None =>
Expand All @@ -85,6 +86,9 @@ class SttpBackendStub[R[_], S] private (
private def wrapResponse[T](r: Response[_]): R[Response[T]] =
rm.unit(r.asInstanceOf[Response[T]])

private def wrapResponse[T](r: R[Response[_]]): R[Response[T]] =
rm.map(r)(_.asInstanceOf[Response[T]])

override def close(): Unit = {}

override def responseMonad: MonadError[R] = rm
Expand All @@ -104,7 +108,13 @@ class SttpBackendStub[R[_], S] private (
def thenRespond[T](body: T): SttpBackendStub[R, S] =
thenRespond(Response[T](Right(body), 200, "OK", Nil, Nil))
def thenRespond[T](resp: => Response[T]): SttpBackendStub[R, S] = {
val m: PartialFunction[Request[_, _], Response[_]] = {
val m: PartialFunction[Request[_, _], R[Response[_]]] = {
case r if p(r) => rm.unit(resp)
}
new SttpBackendStub(rm, matchers.orElse(m), fallback)
}
def thenRespondWithMonad(resp: => R[Response[_]]): SttpBackendStub[R, S] = {
val m: PartialFunction[Request[_, _], R[Response[_]]] = {
case r if p(r) => resp
}
new SttpBackendStub(rm, matchers.orElse(m), fallback)
Expand Down Expand Up @@ -159,13 +169,19 @@ object SttpBackendStub {
PartialFunction.empty,
Some(fallback))

private[sttp] def tryAdjustResponseType[T, U](ra: ResponseAs[T, _],
r: Response[U]): Response[_] = {
r.body match {
case Left(_) => r
case Right(body) =>
val newBody: Any = tryAdjustResponseBody(ra, body).getOrElse(body)
r.copy(body = Right(newBody))
private[sttp] def tryAdjustResponseType[DesiredRType, RType, M[_]](
rm: MonadError[M],
ra: ResponseAs[DesiredRType, _],
m: M[Response[RType]]): M[Response[DesiredRType]] = {
rm.map[Response[RType], Response[DesiredRType]](m) { r =>
r.body match {
case Left(_) => r.asInstanceOf[Response[DesiredRType]]
case Right(body) =>
val newBody: Any = tryAdjustResponseBody(ra, body).getOrElse(body)
r.copy(
body =
Right[String, DesiredRType](newBody.asInstanceOf[DesiredRType]))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import com.softwaremill.sttp._
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.{FlatSpec, Matchers}

import scala.concurrent.Future

class SttpBackendStubTests extends FlatSpec with Matchers with ScalaFutures {
private val testingStub = SttpBackendStub(HttpURLConnectionBackend())
.whenRequestMatches(_.uri.path.startsWith(List("a", "b")))
Expand All @@ -26,6 +28,8 @@ class SttpBackendStubTests extends FlatSpec with Matchers with ScalaFutures {
List("partialAda")) =>
Response(Right("Ada"), 200, "OK", Nil, Nil)
})
.whenRequestMatches(_.uri.port.exists(_ == 8080))
.thenRespondWithMonad(Response(Right("OK from monad"), 200, "OK", Nil, Nil))

"backend stub" should "use the first rule if it matches" in {
implicit val b = testingStub
Expand All @@ -50,6 +54,14 @@ class SttpBackendStubTests extends FlatSpec with Matchers with ScalaFutures {
r.body should be('right)
}

it should "respond with monad with set response" in {
implicit val b = testingStub
val r = sttp.post(uri"http://example.org:8080").send()
r.is200 should be(true)
r.body should be('right)
r.body.right.get should be("OK from monad")
}

it should "use the default response if no rule matches" in {
implicit val b = testingStub
val r = sttp.put(uri"http://example.org/d").send()
Expand Down Expand Up @@ -155,6 +167,25 @@ class SttpBackendStubTests extends FlatSpec with Matchers with ScalaFutures {

}

it should "not hold the calling thread when passed a future monad" in {
val LongTimeMillis = 10000L
val before = System.currentTimeMillis()

implicit val s = SttpBackendStub(new FutureMonad()).whenAnyRequest
.thenRespondWithMonad(Future {
Thread.sleep(LongTimeMillis)
Response(Right("OK"), 200, "", Nil, Nil)
})

sttp
.get(uri"http://example.org")
.send()

val after = System.currentTimeMillis()

(after - before) should be < LongTimeMillis
}

private val testingStubWithFallback = SttpBackendStub
.withFallback(testingStub)
.whenRequestMatches(_.uri.path.startsWith(List("c")))
Expand Down
12 changes: 12 additions & 0 deletions docs/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ It is also possible to match requests by partial function, returning a response.

This approach to testing has one caveat: the responses are not type-safe. That is, the stub backend cannot match on or verify that the type of the response body matches the response body type requested.

Another way to specify the behaviour is passing a response monad to Stub. It is useful if you need to test scenario with slow server, when response should be not returned immediately but after some time.
Example with Futures: ::

implicit val testingBackend = SttpBackendStub(new FutureMonad()).whenAnyRequest
.thenRespondWithMonad(Future {
Thread.sleep(5000)
Response(Right("OK"), 200, "", Nil, Nil)
})

val respFuture = sttp.get(uri"http://example.org").send()
// responseFuture will complete after 10 seconds with "OK" response

Simulating exceptions
---------------------

Expand Down

0 comments on commit 88a3a82

Please sign in to comment.