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 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:


import akka.actor.ActorSystem; import akka.http.javadsl.ConnectHttp; import akka.http.javadsl.Http; import akka.http.javadsl.server.AllDirectives; import akka.http.javadsl.server.Route; import akka.http.javadsl.unmarshalling.StringUnmarshallers; import akka.http.javadsl.server.examples.simple.SimpleServerApp; import akka.stream.ActorMaterializer; import java.io.IOException; public class MyAppService extends AllDirectives { public String add(double x, double y) { return "x + y = " + (x + y); } public Route createRoute() { return get(() -> pathPrefix("calculator", () -> path("add", () -> parameter(StringUnmarshallers.DOUBLE, "x", x -> parameter(StringUnmarshallers.DOUBLE, "y", y -> complete(add(x, y)) ) ) ) ) ); } public static void main(String[] args) throws IOException { final ActorSystem system = ActorSystem.create(); final ActorMaterializer materializer = ActorMaterializer.create(system); final SimpleServerApp app = new SimpleServerApp(); final ConnectHttp host = ConnectHttp.toHost("127.0.0.1"); Http.get(system).bindAndHandle(app.createRoute().flow(system, materializer), host, materializer); System.console().readLine("Type RETURN to exit..."); system.terminate(); } }

MyAppService extends from AllDirectives which brings all of the directives into scope. We define a method called createRoute that provides the routes to serve to bindAndHandle.

Here’s how you would test that service:

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");
    }
}

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:

Inspector 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<T> 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 framework 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.

Testing sealed Routes

The section above describes how to test a “regular” branch of your route structure, which reacts to incoming requests with HTTP response parts or rejections. Sometimes, however, you will want to verify that your service also translates Rejections to HTTP responses in the way you expect.

You do this by calling the seal() method of the Route class. The seal() method applies the logic of ExceptionHandler and RejectionHandler passed as method arguments to all exceptions and rejections coming back from the route, and translates them to the respective HttpResponse.

Testing Route fragments

Since the testkit is request-based, you cannot test requests that are illegal or impossible in HTTP. One such instance is testing a route that begins with the pathEnd directive, such as this:

import akka.http.javadsl.server.AllDirectives;
import akka.http.javadsl.server.Route;

public class MyAppFragment extends AllDirectives {
    public Route createRoute() {
        return
                pathEnd(() ->
                        get(() ->
                                complete("Fragments of imagination")
                        )
                );

    }

}

However, it is impossible to unit test this Route directly using testkit, since it is impossible to create an empty HTTP request. To test this type of route, embed it in a synthetic route in your test, for example:

import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.StatusCodes;
import akka.http.javadsl.server.AllDirectives;
import akka.http.javadsl.server.Route;
import akka.http.javadsl.testkit.JUnitRouteTest;
import akka.http.javadsl.testkit.TestRoute;
import org.junit.Test;

public class TestKitFragmentTest extends JUnitRouteTest {
    class FragmentTester extends AllDirectives {
        public Route createRoute(Route fragment) {
            return
                    pathPrefix("test", () ->
                            fragment
                    );
        }
    }

    TestRoute fragment = testRoute(new MyAppFragment().createRoute());
    TestRoute testRoute = testRoute(new FragmentTester().createRoute(fragment.underlying()));

    @Test
    public void testFragment() {
        testRoute.run(HttpRequest.GET("/test"))
                .assertStatusCode(200)
                .assertEntity("Fragments of imagination");

        testRoute.run(HttpRequest.PUT("/test"))
                .assertStatusCode(StatusCodes.METHOD_NOT_ALLOWED);
    }
}
The source code for this page can be found here.