Skip to content

Commit

Permalink
Merge pull request #38 from softwaremill/stub-match-partial
Browse files Browse the repository at this point in the history
Test stub matching partial function mapping request to response
  • Loading branch information
adamw committed Oct 25, 2017
2 parents 46318ba + 8f2cc79 commit ecbff12
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,17 @@ class SttpBackendStub[R[_], S] private (rm: MonadError[R],
def whenRequestMatches(p: Request[_, _] => Boolean): WhenRequest =
new WhenRequest(p)

def whenRequestMatchesPartial(
partial: PartialFunction[Request[_, _], Response[_]]) = {
val m = Matcher(partial)
val vector: Vector[Matcher[_]] = matchers :+ m
new SttpBackendStub(rm, vector, fallback)
}

override def send[T](request: Request[T, S]): R[Response[T]] = {
matchers
.collectFirst {
case matcher if matcher(request) => matcher.response
case matcher: Matcher[T] if matcher(request) => matcher.response(request).get
} match {
case Some(response) => wrapResponse(response)
case None =>
Expand Down Expand Up @@ -64,8 +71,11 @@ class SttpBackendStub[R[_], S] private (rm: MonadError[R],
thenRespond(Response[Nothing](Left(msg), code, Nil, Nil))
def thenRespond[T](body: T): SttpBackendStub[R, S] =
thenRespond(Response[T](Right(body), 200, Nil, Nil))
def thenRespond[T](resp: Response[T]): SttpBackendStub[R, S] =
new SttpBackendStub(rm, matchers :+ Matcher(p, resp), fallback)
def thenRespond[T](resp: Response[T]): SttpBackendStub[R, S] = {
val m = Matcher[T](p, resp)
new SttpBackendStub(rm, matchers :+ m, fallback)

}
}
}

Expand Down Expand Up @@ -101,9 +111,22 @@ object SttpBackendStub {
private val DefaultResponse =
Response[Nothing](Left("Not Found"), 404, Nil, Nil)

private case class Matcher[T](p: Request[T, _] => Boolean,
response: Response[T]) {
def apply(request: Request[_, _]): Boolean =
p(request.asInstanceOf[Request[T, _]])
private case class Matcher[T](p: PartialFunction[Request[T, _], Response[_]]) {

def apply(request: Request[T, _]): Boolean =
p.isDefinedAt(request)

def response[S](request: Request[T, S]): Option[Response[T]] = {
p.lift(request).asInstanceOf[Option[Response[T]]]
}
}

private object Matcher {

def apply[T](p: Request[T, _] => Boolean, response: Response[T]) = {
new Matcher[T]({
case r if p(r) => response
})
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ class SttpBackendStubTests extends FlatSpec with Matchers with ScalaFutures {
.thenRespond(10)
.whenRequestMatches(_.method == Method.GET)
.thenRespondServerError()
.whenRequestMatchesPartial({
case r if r.method == Method.POST && r.uri.path.endsWith(List("partial10")) => Response(Right(10), 200, Nil, Nil)
case r if r.method == Method.POST && r.uri.path.endsWith(List("partialAda")) => Response(Right("Ada"), 200, Nil, Nil)
})

"backend stub" should "use the first rule if it matches" in {
implicit val b = testingStub
Expand All @@ -38,7 +42,7 @@ class SttpBackendStubTests extends FlatSpec with Matchers with ScalaFutures {

it should "use the default response if no rule matches" in {
implicit val b = testingStub
val r = sttp.post(uri"http://example.org/d").send()
val r = sttp.put(uri"http://example.org/d").send()
r.code should be(404)
}

Expand All @@ -49,6 +53,17 @@ class SttpBackendStubTests extends FlatSpec with Matchers with ScalaFutures {
r.futureValue.code should be(404)
}

it should "use rules in partial function" in {
implicit val s = testingStub
val r = sttp.post(uri"http://example.org/partial10").send()
r.is200 should be(true)
r.body should be(Right(10))

val ada = sttp.post(uri"http://example.org/partialAda").send()
ada.is200 should be(true)
ada.body should be(Right("Ada"))
}

val testingStubWithFallback = SttpBackendStub
.withFallback(testingStub)
.whenRequestMatches(_.uri.path.startsWith(List("c")))
Expand Down
14 changes: 14 additions & 0 deletions docs/backends/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ For example::
val response2 = sttp.post(uri"http://example.org/d/e").send()
// response2.code will be 500

It is also possible to match request by partial function, returning a response. E.g.:

implicit val testingBackend = SttpBackendStub(HttpURLConnectionBackend())
.whenRequestMatchesPartial({
case r if r.uri.path.endsWith(List("partial10")) => Response(Right(10), 200, Nil, Nil)
case r if r.uri.path.endsWith(List("partialAda")) => Response(Right("Ada"), 200, Nil, Nil)
})

val response1 = sttp.get(uri"http://example.org/partial10").send()
// response1.body will be Right(10)

val response2 = sttp.post(uri"http://example.org/partialAda").send()
// response2.body will be Right("Ada")

However, this approach has one caveat: the responses are not type-safe. That is, the backend cannot match on or verify that the type included in the response matches the response type requested.

It is also possible to create a stub backend which delegates calls to another (possibly "real") backend if none of the specified predicates match a request. This can be useful during development, to partially stub a yet incomplete API with which we integrate::
Expand Down
2 changes: 1 addition & 1 deletion docs/credits.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Credits
* `Piotr Buda <https://github.com/pbuda>`_
* `Piotr Gabara <https://github.com/bhop>`_
* `Gabriele Petronella <https://github.com/gabro>`_

* `Paweł Stawicki <https://github.com/amorfis>`_

0 comments on commit ecbff12

Please sign in to comment.