From bb4f2d27fa56ced1f565d9326a08fd5a87e2202b Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 9 Oct 2017 12:01:33 +0200 Subject: Testing backend --- .../sttp/testing/SttpBackendStub.scala | 90 ++++++++++++++++++++++ .../sttp/testing/SttpBackendStubTests.scala | 51 ++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 core/src/main/scala/com/softwaremill/sttp/testing/SttpBackendStub.scala create mode 100644 core/src/test/scala/com/softwaremill/sttp/testing/SttpBackendStubTests.scala (limited to 'core') 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) + } +} -- cgit v1.2.3