aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md18
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala39
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala21
3 files changed, 67 insertions, 11 deletions
diff --git a/README.md b/README.md
index 1adbbf6..336531c 100644
--- a/README.md
+++ b/README.md
@@ -560,6 +560,23 @@ 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:
+
+```scala
+implicit val testingBackend = SttpBackendStub.withFallback(HttpURLConnectionBackend())
+ .whenRequestMatches(_.uri.path.startsWith(List("a")))
+ .thenRespond("I'm a STUB!")
+
+val response1 = sttp.get(uri"http://api.internal/a").send()
+// response1.body will be Right("I'm a STUB")
+
+val response2 = sttp.post(uri"http://api.internal/b").send()
+// response2 will be whatever a "real" network call to api.internal/b returns
+```
+
## Notes
* the encoding for `String`s defaults to `utf-8`.
@@ -593,3 +610,4 @@ and pick a task you'd like to work on!
* [Bjørn Madsen](https://github.com/aeons)
* [Piotr Buda](https://github.com/pbuda)
* [Piotr Gabara](https://github.com/bhop)
+* [Gabriele Petronella](https://github.com/gabro)
diff --git a/core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala b/core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala
index 5724264..aa04ef3 100644
--- a/core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala
+++ b/core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala
@@ -19,7 +19,8 @@ import scala.language.higherKinds
* a response is specified with the incorrect body type.
*/
class SttpBackendStub[R[_], S] private (rm: MonadError[R],
- matchers: Vector[Matcher[_]])
+ matchers: Vector[Matcher[_]],
+ fallback: Option[SttpBackend[R, S]])
extends SttpBackend[R, S] {
/**
@@ -31,15 +32,22 @@ class SttpBackendStub[R[_], S] private (rm: MonadError[R],
new WhenRequest(p)
override def send[T](request: Request[T, S]): R[Response[T]] = {
- val response = matchers
+ matchers
.collectFirst {
case matcher if matcher(request) => matcher.response
- }
- .getOrElse(DefaultResponse)
-
- rm.unit(response.asInstanceOf[Response[T]])
+ } match {
+ case Some(response) => wrapResponse(response)
+ case None =>
+ fallback match {
+ case None => wrapResponse(DefaultResponse)
+ case Some(fb) => fb.send(request)
+ }
+ }
}
+ private def wrapResponse[T](r: Response[_]): R[Response[T]] =
+ rm.unit(r.asInstanceOf[Response[T]])
+
override def close(): Unit = {}
override def responseMonad: MonadError[R] = rm
@@ -57,7 +65,7 @@ class SttpBackendStub[R[_], S] private (rm: MonadError[R],
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))
+ new SttpBackendStub(rm, matchers :+ Matcher(p, resp), fallback)
}
}
@@ -71,16 +79,27 @@ object SttpBackendStub {
* [[https://stackoverflow.com/questions/46642623/cannot-infer-contravariant-nothing-type-parameter]].
*/
def apply[R[_], S, S2 <: S](c: SttpBackend[R, S]): SttpBackendStub[R, S2] =
- new SttpBackendStub[R, S2](c.responseMonad, Vector.empty)
+ new SttpBackendStub[R, S2](c.responseMonad, Vector.empty, None)
/**
* Create a stub backend using the given response monad (which determines
* how requests are wrapped), and any stream type.
*/
def apply[R[_], S](responseMonad: MonadError[R]): SttpBackendStub[R, S] =
- new SttpBackendStub[R, S](responseMonad, Vector.empty)
+ new SttpBackendStub[R, S](responseMonad, Vector.empty, None)
+
+ /**
+ * Create a stub backend which delegates send requests to the given fallback
+ * backend, if the request doesn't match any of the specified predicates.
+ */
+ def withFallback[R[_], S, S2 <: S](
+ fallback: SttpBackend[R, S]): SttpBackendStub[R, S2] =
+ new SttpBackendStub[R, S2](fallback.responseMonad,
+ Vector.empty,
+ Some(fallback))
- private val DefaultResponse = Response[Nothing](Left(""), 404, Nil, Nil)
+ private val DefaultResponse =
+ Response[Nothing](Left("Not Found"), 404, Nil, Nil)
private case class Matcher[T](p: Request[T, _] => Boolean,
response: Response[T]) {
diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala b/core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala
index 538dd35..b80ae76 100644
--- a/core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala
+++ b/core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala
@@ -13,7 +13,7 @@ class SttpBackendStubTests extends FlatSpec with Matchers with ScalaFutures {
.whenRequestMatches(_.method == Method.GET)
.thenRespondServerError()
- it should "use the first rule if it matches" in {
+ "backend stub" should "use the first rule if it matches" in {
implicit val b = testingStub
val r = sttp.get(uri"http://example.org/a/b/c").send()
r.is200 should be(true)
@@ -48,4 +48,23 @@ class SttpBackendStubTests extends FlatSpec with Matchers with ScalaFutures {
val r = sttp.post(uri"http://example.org").send()
r.futureValue.code should be(404)
}
+
+ val testingStubWithFallback = SttpBackendStub
+ .withFallback(testingStub)
+ .whenRequestMatches(_.uri.path.startsWith(List("c")))
+ .thenRespond("ok")
+
+ "backend stub with fallback" should "use the stub when response for a request is defined" in {
+ implicit val b = testingStubWithFallback
+
+ val r = sttp.post(uri"http://example.org/c").send()
+ r.body should be(Right("ok"))
+ }
+
+ it should "delegate to the fallback for unhandled requests" in {
+ implicit val b = testingStubWithFallback
+
+ val r = sttp.post(uri"http://example.org/a/b").send()
+ r.is200 should be(true)
+ }
}