diff options
-rw-r--r-- | README.md | 30 | ||||
-rw-r--r-- | core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala | 90 | ||||
-rw-r--r-- | core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala | 51 |
3 files changed, 171 insertions, 0 deletions
@@ -530,6 +530,36 @@ parameter. See [akka-http docs](http://doc.akka.io/docs/akka-http/current/scala/ * OkHttp: create a custom client modifying the SSL settings as described [on the wiki](https://github.com/square/okhttp/wiki/HTTPS) +## Testing + +If you need a stub backend for use in tests instead of a "real" backend (you +probably don't want to make HTTP calls during unit tests), you can use the +`SttpBackendStub` class. It allows specifying how the backend should respond +to requests matching given predicates. + +A backend stub can be created using an instance of a "real" backend, or by +explicitly giving the response wrapper monad and supported streams type. + +For example: + +```scala +implicit val testingBackend = SttpBackendStub(HttpURLConnectionBackend()) + .whenRequestMatches(_.uri.path.startsWith(List("a", "b"))) + .thenRespond("Hello there!") + .whenRequestMatches(_.method == Method.POST) + .thenRespondServerError() + +val response1 = sttp.get(uri"http://example.org/a/b/c").send() +// response1.body will be Right("Hello there") + +val response2 = sttp.post(uri"http://example.org/d/e").send() +// response2.code will be 500 +``` + +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. + ## Notes * the encoding for `String`s defaults to `utf-8`. diff --git a/core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala b/core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala new file mode 100644 index 0000000..5724264 --- /dev/null +++ b/core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala @@ -0,0 +1,90 @@ +package com.softwaremill.sttp.testing + +import com.softwaremill.sttp.testing.SttpBackendStub._ +import com.softwaremill.sttp.{MonadError, Request, Response, SttpBackend} + +import scala.language.higherKinds + +/** + * A stub backend to use in tests. + * + * The stub can be configured to respond with a given response if the + * request matches a predicate (see the [[whenRequestMatches()]] method). + * + * Note however, that this is not type-safe with respect to the type of the + * response body - the stub doesn't have a way to check if the type of the + * body in the configured response is the same as the one specified by the + * request. Hence, the predicates can match requests basing on the URI + * or headers. A [[ClassCastException]] might occur if for a given request, + * a response is specified with the incorrect body type. + */ +class SttpBackendStub[R[_], S] private (rm: MonadError[R], + matchers: Vector[Matcher[_]]) + extends SttpBackend[R, S] { + + /** + * Specify how the stub backend should respond to requests matching the + * given predicate. Note that the stubs are immutable, and each new + * specification that is added yields a new stub instance. + */ + def whenRequestMatches(p: Request[_, _] => Boolean): WhenRequest = + new WhenRequest(p) + + override def send[T](request: Request[T, S]): R[Response[T]] = { + val response = matchers + .collectFirst { + case matcher if matcher(request) => matcher.response + } + .getOrElse(DefaultResponse) + + rm.unit(response.asInstanceOf[Response[T]]) + } + + override def close(): Unit = {} + + override def responseMonad: MonadError[R] = rm + + class WhenRequest(p: Request[_, _] => Boolean) { + def thenRespondOk(): SttpBackendStub[R, S] = + thenRespondWithCode(200) + def thenRespondNotFound(): SttpBackendStub[R, S] = + thenRespondWithCode(404, "Not found") + def thenRespondServerError(): SttpBackendStub[R, S] = + thenRespondWithCode(500, "Internal server error") + def thenRespondWithCode(code: Int, + msg: String = ""): SttpBackendStub[R, S] = + 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)) + } +} + +object SttpBackendStub { + + /** + * Create a stub backend for testing, which uses the same response wrappers + * and supports the same stream type as the given "real" backend. + * + * @tparam S2 This is a work-around for the problem described here: + * [[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) + + /** + * 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) + + private val DefaultResponse = Response[Nothing](Left(""), 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, _]]) + } +} diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala b/core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala new file mode 100644 index 0000000..538dd35 --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala @@ -0,0 +1,51 @@ +package com.softwaremill.sttp.testing + +import com.softwaremill.sttp._ +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.{FlatSpec, Matchers} + +class SttpBackendStubTests extends FlatSpec with Matchers with ScalaFutures { + val testingStub = SttpBackendStub(HttpURLConnectionBackend()) + .whenRequestMatches(_.uri.path.startsWith(List("a", "b"))) + .thenRespondOk() + .whenRequestMatches(_.uri.paramsMap.get("p").contains("v")) + .thenRespond(10) + .whenRequestMatches(_.method == Method.GET) + .thenRespondServerError() + + it 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) + r.body should be('left) + } + + it should "use subsequent rules if the first doesn't match" in { + implicit val b = testingStub + val r = sttp + .get(uri"http://example.org/d?p=v") + .response(asString.map(_.toInt)) + .send() + r.body should be(Right(10)) + } + + it should "use the first specified rule if multiple match" in { + implicit val b = testingStub + val r = sttp.get(uri"http://example.org/a/b/c?p=v").send() + r.is200 should be(true) + r.body should be('left) + } + + 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() + r.code should be(404) + } + + it should "wrap responses in the desired monad" in { + import scala.concurrent.ExecutionContext.Implicits.global + implicit val b = SttpBackendStub(new FutureMonad()) + val r = sttp.post(uri"http://example.org").send() + r.futureValue.code should be(404) + } +} |