diff --git a/akka-docs-dev/rst/java/code/docs/http/javadsl/testkit/MyAppService.java b/akka-docs-dev/rst/java/code/docs/http/javadsl/testkit/MyAppService.java new file mode 100644 index 0000000000..28621b9e3a --- /dev/null +++ b/akka-docs-dev/rst/java/code/docs/http/javadsl/testkit/MyAppService.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package docs.http.javadsl.testkit; + +//#simple-app +import akka.http.javadsl.server.*; +import akka.http.javadsl.server.values.Parameters; + +public class MyAppService extends HttpApp { + RequestVal x = Parameters.doubleValue("x"); + RequestVal y = Parameters.doubleValue("y"); + + public RouteResult add(RequestContext ctx, double x, double y) { + return ctx.complete("x + y = " + (x + y)); + } + + @Override + public Route createRoute() { + return + route( + get( + pathPrefix("calculator").route( + path("add").route( + handleReflectively(this, "add", x, y) + ) + ) + ) + ); + } +} +//#simple-app \ No newline at end of file diff --git a/akka-docs-dev/rst/java/code/docs/http/javadsl/testkit/TestkitExampleTest.java b/akka-docs-dev/rst/java/code/docs/http/javadsl/testkit/TestkitExampleTest.java new file mode 100644 index 0000000000..6f97671ebc --- /dev/null +++ b/akka-docs-dev/rst/java/code/docs/http/javadsl/testkit/TestkitExampleTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package docs.http.javadsl.testkit; + +//#simple-app-testing +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.StatusCodes; +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.testkit.TestRoute; +import org.junit.Test; + +public class TestkitExampleTest extends JUnitRouteTest { + TestRoute appRoute = testRoute(new MyAppService().createRoute()); + + @Test + public void testCalculatorAdd() { + // test happy path + appRoute.run(HttpRequest.GET("/calculator/add?x=4.2&y=2.3")) + .assertStatusCode(200) + .assertEntity("x + y = 6.5"); + + // test responses to potential errors + appRoute.run(HttpRequest.GET("/calculator/add?x=3.2")) + .assertStatusCode(StatusCodes.NOT_FOUND) // 404 + .assertEntity("Request is missing required query parameter 'y'"); + + // test responses to potential errors + appRoute.run(HttpRequest.GET("/calculator/add?x=3.2&y=three")) + .assertStatusCode(StatusCodes.BAD_REQUEST) + .assertEntity("The query parameter 'y' was malformed:\n" + + "'three' is not a valid 64-bit floating point value"); + } +} +//#simple-app-testing \ No newline at end of file diff --git a/akka-docs-dev/rst/java/http/routing-dsl/index.rst b/akka-docs-dev/rst/java/http/routing-dsl/index.rst index 913f2dfc64..430cf5469e 100644 --- a/akka-docs-dev/rst/java/http/routing-dsl/index.rst +++ b/akka-docs-dev/rst/java/http/routing-dsl/index.rst @@ -14,4 +14,5 @@ To use the high-level API you need to add a dependency to the ``akka-http-experi request-vals/index handlers marshalling + testkit json-jackson-support \ No newline at end of file diff --git a/akka-docs-dev/rst/java/http/routing-dsl/overview.rst b/akka-docs-dev/rst/java/http/routing-dsl/overview.rst index 58ae27999d..529b2cddec 100644 --- a/akka-docs-dev/rst/java/http/routing-dsl/overview.rst +++ b/akka-docs-dev/rst/java/http/routing-dsl/overview.rst @@ -68,4 +68,9 @@ to HTTP entities. Read more about :ref:`marshalling-java`. +akka-http contains a testkit that simplifies testing routes. It allows to run test-requests against (sub-)routes +quickly without running them over the network and helps with writing assertions on HTTP response properties. + +Read more about :ref:`http-testkit-java`. + .. _DRY: http://en.wikipedia.org/wiki/Don%27t_repeat_yourself diff --git a/akka-docs-dev/rst/java/http/routing-dsl/testkit.rst b/akka-docs-dev/rst/java/http/routing-dsl/testkit.rst new file mode 100644 index 0000000000..36fce8c213 --- /dev/null +++ b/akka-docs-dev/rst/java/http/routing-dsl/testkit.rst @@ -0,0 +1,80 @@ +.. _http-testkit-java: + +Route Testkit +============= + +akka-http has a testkit that provides a convenient way of testing your routes with JUnit. It allows +running requests against a route (without hitting the network) and provides means to assert against +response properties in a compact way. + +To use the testkit you need to take these steps: + +* add a dependency to the ``akka-http-testkit-experimental`` module +* derive the test class from ``JUnitRouteTest`` +* wrap the route under test with ``RouteTest.testRoute`` to create a ``TestRoute`` +* run requests against the route using ``TestRoute.run(request)`` which will return + a ``TestResponse`` +* use the methods of ``TestResponse`` to assert on properties of the response + +Example +------- + +To see the testkit in action consider the following simple calculator app service: + +.. includecode:: ../../code/docs/http/javadsl/testkit/MyAppService.java + :include: simple-app + +The app extends from ``HttpApp`` which brings all of the directives into scope. Method ``createRoute`` +needs to be implemented to return the complete route of the app. + +Here's how you would test that service: + +.. includecode:: ../../code/docs/http/javadsl/testkit/TestkitExampleTest.java + :include: simple-app-testing + + +Writing Asserting against the HttpResponse +------------------------------------------ + +The testkit supports a fluent DSL to write compact assertions on the response by chaining assertions +using "dot-syntax". To simplify working with streamed responses the entity of the response is first "strictified", i.e. +entity data is collected into a single ``ByteString`` and provided the entity is supplied as an ``HttpEntityStrict``. This +allows to write several assertions against the same entity data which wouldn't (necessarily) be possible for the +streamed version. + +All of the defined assertions provide HTTP specific error messages aiding in diagnosing problems. + +Currently, these methods are defined on ``TestResponse`` to assert on the response: + +=================================================================== ======================================================================= +Assertion Description +=================================================================== ======================================================================= +``assertStatusCode(int expectedCode)`` Asserts that the numeric response status code equals the expected one +``assertStatusCode(StatusCode expectedCode)`` Asserts that the response ``StatusCode`` equals the expected one +``assertMediaType(String expectedType)`` Asserts that the media type part of the response's content type matches + the given String +``assertMediaType(MediaType expectedType)`` Asserts that the media type part of the response's content type matches + the given ``MediaType`` +``assertEntity(String expectedStringContent)`` Asserts that the entity data interpreted as UTF8 equals the expected + String +``assertEntityBytes(ByteString expectedBytes)`` Asserts that the entity data bytes equal the expected ones +``assertEntityAs(Unmarshaller unmarshaller, expectedValue: T)`` Asserts that the entity data if unmarshalled with the given marshaller + equals the given value +``assertHeaderExists(HttpHeader expectedHeader)`` Asserts that the response contains an HttpHeader instance equal to the + expected one +``assertHeaderKindExists(String expectedHeaderName)`` Asserts that the response contains a header with the expected name +``assertHeader(String name, String expectedValue)`` Asserts that the response contains a header with the given name and + value. +=================================================================== ======================================================================= + +It's, of course, possible to use any other means of writing assertions by inspecting the properties the response +manually. As written above, ``TestResponse.entity`` and ``TestResponse.response`` return strict versions of the +entity data. + +Supporting Custom Test Frameworks +--------------------------------- + +Adding support for a custom test framework is achieved by creating new superclass analogous to +``JUnitRouteTest`` for writing tests with the custom test framwork deriving from ``akka.http.javadsl.testkit.RouteTest`` +and implementing its abstract methods. This will allow users of the test framework to use ``testRoute`` and +to write assertions using the assertion methods defined on ``TestResponse``. \ No newline at end of file diff --git a/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/RouteTest.scala b/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/RouteTest.scala index d917ae4999..6b171a9fee 100644 --- a/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/RouteTest.scala +++ b/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/RouteTest.scala @@ -39,6 +39,9 @@ abstract class RouteTest extends AllDirectives { } } + /** + * Wraps a list of route alternatives with testing support. + */ @varargs def testRoute(first: Route, others: Route*): TestRoute = new TestRoute { diff --git a/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/TestResponse.scala b/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/TestResponse.scala index 449a766362..dc976422ad 100644 --- a/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/TestResponse.scala +++ b/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/TestResponse.scala @@ -21,49 +21,124 @@ import akka.http.javadsl.model._ * A wrapper for responses */ abstract class TestResponse(_response: HttpResponse, awaitAtMost: FiniteDuration)(implicit ec: ExecutionContext, materializer: ActorMaterializer) { - lazy val entity: HttpEntityStrict = - _response.entity.toStrict(awaitAtMost).awaitResult(awaitAtMost) + /** + * Returns the strictified entity of the response. It will be strictified on first access. + */ + lazy val entity: HttpEntityStrict = _response.entity.toStrict(awaitAtMost).awaitResult(awaitAtMost) + + /** + * Returns a copy of the underlying response with the strictified entity. + */ lazy val response: HttpResponse = _response.withEntity(entity) - // FIXME: add header getters / assertions - + /** + * Returns the media-type of the the response's content-type + */ def mediaType: MediaType = extractFromResponse(_.entity.contentType.mediaType) + + /** + * Returns a string representation of the media-type of the the response's content-type + */ def mediaTypeString: String = mediaType.toString + + /** + * Returns the bytes of the response entity + */ def entityBytes: ByteString = entity.data() + + /** + * Returns the entity of the response unmarshalled with the given ``Unmarshaller``. + */ def entityAs[T](unmarshaller: Unmarshaller[T]): T = Unmarshal(response) .to(unmarshaller.asInstanceOf[UnmarshallerImpl[T]].scalaUnmarshaller(ec, materializer), ec) .awaitResult(awaitAtMost) + + /** + * Returns the entity of the response interpreted as an UTF-8 encoded string. + */ def entityAsString: String = entity.data().utf8String + + /** + * Returns the [[StatusCode]] of the response. + */ def status: StatusCode = response.status.asJava + + /** + * Returns the numeric status code of the response. + * @return + */ def statusCode: Int = response.status.intValue + + /** + * Returns the first header of the response which is of the given class. + */ def header[T <: HttpHeader](clazz: Class[T]): T = response.header(ClassTag(clazz)) .getOrElse(fail(s"Expected header of type ${clazz.getSimpleName} but wasn't found.")) + /** + * Assert on the numeric status code. + */ def assertStatusCode(expected: Int): TestResponse = assertStatusCode(StatusCodes.get(expected)) + + /** + * Assert on the status code. + */ def assertStatusCode(expected: StatusCode): TestResponse = assertEqualsKind(expected, status, "status code") + + /** + * Assert on the media type of the response. + */ def assertMediaType(expected: String): TestResponse = assertEqualsKind(expected, mediaTypeString, "media type") + + /** + * Assert on the media type of the response. + */ def assertMediaType(expected: MediaType): TestResponse = assertEqualsKind(expected, mediaType, "media type") + + /** + * Assert on the response entity to be a UTF8 representation of the given string. + */ def assertEntity(expected: String): TestResponse = assertEqualsKind(expected, entityAsString, "entity") + + /** + * Assert on the response entity to equal the given bytes. + */ def assertEntityBytes(expected: ByteString): TestResponse = assertEqualsKind(expected, entityBytes, "entity") + + /** + * Assert on the response entity to equal the given object after applying an [[Unmarshaller]]. + */ def assertEntityAs[T <: AnyRef](unmarshaller: Unmarshaller[T], expected: T): TestResponse = assertEqualsKind(expected, entityAs(unmarshaller), "entity") + + /** + * Assert that a given header instance exists in the response. + */ def assertHeaderExists(expected: HttpHeader): TestResponse = { assertTrue(response.headers.exists(_ == expected), s"Header $expected was missing.") this } + + /** + * Assert that a header of the given type exists. + */ def assertHeaderKindExists(name: String): TestResponse = { val lowercased = name.toRootLowerCase assertTrue(response.headers.exists(_.is(lowercased)), s"Expected `$name` header was missing.") this } + + /** + * Assert that a header of the given name and value exists. + */ def assertHeaderExists(name: String, value: String): TestResponse = { val lowercased = name.toRootLowerCase val headers = response.headers.filter(_.is(lowercased))