Route TestKit

One of Akka HTTP’s design goals is good testability of the created services. For services built with the Routing DSL Akka HTTP provides a dedicated testkit that makes efficient testing of route logic easy and convenient. This “route test DSL” is made available with the akka-http-testkit module.

Dependency

The Akka dependencies are available from Akka’s library repository. To access them there, you need to configure the URL for this repository.

sbt
resolvers += "Akka library repository".at("https://repo.akka.io/maven")
Gradle
repositories {
    mavenCentral()
    maven {
        url "https://repo.akka.io/maven"
    }
}
Maven
<project>
  ...
  <repositories>
    <repository>
      <id>akka-repository</id>
      <name>Akka library repository</name>
      <url>https://repo.akka.io/maven</url>
    </repository>
  </repositories>
</project>

To use Akka HTTP TestKit, add the module to your project:

sbt
val AkkaVersion = "2.9.3"
val AkkaHttpVersion = "10.6.3"
libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-stream-testkit" % AkkaVersion,
  "com.typesafe.akka" %% "akka-http-testkit" % AkkaHttpVersion
)
Gradle
def versions = [
  AkkaVersion: "2.9.3",
  ScalaBinary: "2.13"
]
dependencies {
  implementation platform("com.typesafe.akka:akka-http-bom_${versions.ScalaBinary}:10.6.3")

  implementation "com.typesafe.akka:akka-stream-testkit_${versions.ScalaBinary}:${versions.AkkaVersion}"
  implementation "com.typesafe.akka:akka-http-testkit_${versions.ScalaBinary}"
}
Maven
<properties>
  <akka.version>2.9.3</akka.version>
  <scala.binary.version>2.13</scala.binary.version>
</properties>
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.typesafe.akka</groupId>
      <artifactId>akka-http-bom_${scala.binary.version}</artifactId>
      <version>10.6.3</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
<dependencies>
  <dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-stream-testkit_${scala.binary.version}</artifactId>
    <version>${akka.version}</version>
  </dependency>
  <dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-http-testkit_${scala.binary.version}</artifactId>
  </dependency>
</dependencies>

Usage

Here is an example of what a simple test with the routing testkit might look like using the built-in support for scalatest and specs2:

ScalaTest
sourceimport akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.testkit.ScalatestRouteTest
import akka.http.scaladsl.server._
import Directives._
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

class FullTestKitExampleSpec extends AnyWordSpec with Matchers with ScalatestRouteTest {

  val smallRoute =
    get {
      concat(
        pathSingleSlash {
          complete {
            "Captain on the bridge!"
          }
        },
        path("ping") {
          complete("PONG!")
        }
      )
    }

  "The service" should {

    "return a greeting for GET requests to the root path" in {
      // tests:
      Get() ~> smallRoute ~> check {
        responseAs[String] shouldEqual "Captain on the bridge!"
      }
    }

    "return a 'PONG!' response for GET requests to /ping" in {
      // tests:
      Get("/ping") ~> smallRoute ~> check {
        responseAs[String] shouldEqual "PONG!"
      }
    }

    "leave GET requests to other paths unhandled" in {
      // tests:
      Get("/kermit") ~> smallRoute ~> check {
        handled shouldBe false
      }
    }

    "return a MethodNotAllowed error for PUT requests to the root path" in {
      // tests:
      Put() ~> Route.seal(smallRoute) ~> check {
        status shouldEqual StatusCodes.MethodNotAllowed
        responseAs[String] shouldEqual "HTTP method not allowed, supported methods: GET"
      }
    }
  }
}
specs2
sourceimport org.specs2.mutable.Specification
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.testkit.Specs2RouteTest
import akka.http.scaladsl.server._
import Directives._

class FullSpecs2TestKitExampleSpec extends Specification with Specs2RouteTest {

  val smallRoute =
    get {
      concat(
        pathSingleSlash {
          complete {
            "Captain on the bridge!"
          }
        },
        path("ping") {
          complete("PONG!")
        }
      )
    }

  "The service" should {

    "return a greeting for GET requests to the root path" in {
      // tests:
      Get() ~> smallRoute ~> check {
        responseAs[String] shouldEqual "Captain on the bridge!"
      }
    }

    "return a 'PONG!' response for GET requests to /ping" in {
      // tests:
      Get("/ping") ~> smallRoute ~> check {
        responseAs[String] shouldEqual "PONG!"
      }
    }

    "leave GET requests to other paths unhandled" in {
      // tests:
      Get("/kermit") ~> smallRoute ~> check {
        handled should beFalse
      }
    }

    "return a MethodNotAllowed error for PUT requests to the root path" in {
      // tests:
      Put() ~> Route.seal(smallRoute) ~> check {
        status shouldEqual StatusCodes.MethodNotAllowed
        responseAs[String] shouldEqual "HTTP method not allowed, supported methods: GET"
      }
    }
  }
}

The basic structure of a test built with the testkit is this (expression placeholder in all-caps):

REQUEST ~> ROUTE ~> check {
  ASSERTIONS
}

In this template REQUEST is an expression evaluating to an HttpRequest instance. In most cases your test will, in one way or another, extend from RouteTest which itself mixes in the akka.http.scaladsl.client.RequestBuilding trait, which gives you a concise and convenient way of constructing test requests. [1]

ROUTE is an expression evaluating to a Route. You can specify one inline or simply refer to the route structure defined in your service.

The final element of the ~> chain is a check call, which takes a block of assertions as parameter. In this block you define your requirements onto the result produced by your route after having processed the given request. Typically you use one of the defined “inspectors” to retrieve a particular element of the routes response and express assertions against it using the test DSL provided by your test framework. For example, with scalatest, in order to verify that your route responds to the request with a status 200 response, you’d use the status inspector and express an assertion like this:

status shouldEqual 200

The following inspectors are defined:

Table of Inspectors

Inspector Description
charset: HttpCharset Identical to contentType.charset
chunks: Seq[HttpEntity.ChunkStreamPart] Returns the entity chunks produced by the route. If the entity is not chunked returns Nil.
closingExtension: String Returns chunk extensions the route produced with its last response chunk. If the response entity is unchunked returns the empty string.
contentType: ContentType Identical to responseEntity.contentType
definedCharset: Option[HttpCharset] Identical to contentType.definedCharset
entityAs[T :FromEntityUnmarshaller]: T Unmarshals the response entity using the in-scope FromEntityUnmarshaller for the given type. Any errors in the process trigger a test failure.
handled: Boolean Indicates whether the route produced an HttpResponse for the request. If the route rejected the request handled evaluates to false.
header(name: String): Option[HttpHeader] Returns the response header with the given name or None if no such header is present in the response.
header[T <: HttpHeader]: Option[T] Identical to response.header[T]
headers: Seq[HttpHeader] Identical to response.headers
mediaType: MediaType Identical to contentType.mediaType
rejection: Rejection The rejection produced by the route. If the route did not produce exactly one rejection a test failure is triggered.
rejections: Seq[Rejection] The rejections produced by the route. If the route did not reject the request a test failure is triggered.
response: HttpResponse The HttpResponse returned by the route. If the route did not return an HttpResponse instance (e.g. because it rejected the request) a test failure is triggered.
responseAs[T: FromResponseUnmarshaller]: T Unmarshals the response entity using the in-scope FromResponseUnmarshaller for the given type. Any errors in the process trigger a test failure.
responseEntity: HttpEntity Returns the response entity.
status: StatusCode Identical to response.status
trailer: Seq[HttpHeader] Returns the list of trailer headers the route produced with its last chunk. If the response entity is unchunked returns Nil.

[1] If the request URI is relative it will be made absolute using an implicitly available instance of DefaultHostInfo whose value is “http://example.com” by default. This mirrors the behavior of akka-http-core which always produces absolute URIs for incoming request based on the request URI and the Host-header of the request. You can customize this behavior by bringing a custom instance of DefaultHostInfo into scope.

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 Route.seal() method. The Route.seal() method applies the logic of the in-scope ExceptionHandler and RejectionHandler to all exceptions and rejections coming back from the route, and translates them to the respective HttpResponse.

Note that explicit call on the Route.seal method is needed in test code, but in your application code it is not necessary. As described in Sealing a Route, your application code only needs to bring implicit rejection and exception handlers in scope.

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

Scala
sourcepathEnd {
  get {
    complete {
      "Fragments of imagination"
    }
  }
}
Java
sourcepathEnd(() ->
        get(() ->
                complete("Fragments of imagination")
        )
);

You might create a route such as this to be able to compose it into another route such as:

Scala
sourceimport akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route

object RouteFragment {
  val route: Route = pathEnd {
    get {
      complete("example")
    }
  }
}

object API {
  pathPrefix("version") {
    RouteFragment.route
  }
}
Java
sourceimport 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, such as testRoute in the example above.

This is what the full working test looks like:

Scala
sourceimport akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.testkit.ScalatestRouteTest
import akka.http.scaladsl.server._
import Directives._
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

class TestKitFragmentSpec extends AnyWordSpec with Matchers with ScalatestRouteTest {

  val routeFragment =
      pathEnd {
        get {
          complete {
            "Fragments of imagination"
          }
        }
      }

  // Synthetic route to enable pathEnd testing
  val testRoute = {
    pathPrefix("test") {
      routeFragment
    }
  }

  "The service" should {
    "return a greeting for GET requests" in {
      // tests:
      Get("/test") ~> testRoute ~> check {
        responseAs[String] shouldEqual "Fragments of imagination"
      }
    }

    "return a MethodNotAllowed error for PUT requests to the root path" in {
      // tests:
      Put("/test") ~> Route.seal(testRoute) ~> check {
        status shouldEqual StatusCodes.MethodNotAllowed
      }
    }
  }
}
Java
sourceimport 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);
    }
}

Accounting for Slow Test Systems

The timeouts you consciously defined on your lightning fast development environment might be too tight for your, most probably, high-loaded Continuous Integration server, invariably causing spurious failures. To account for such situations, timeout durations can be scaled by a given factor on such environments. Check the Akka Docs for further information.

Increase Timeout

The default timeout when testing your routes using the testkit is 1 second second. Sometimes, though, this might not be enough. In order to extend this default timeout, to say 5 seconds, just add the following implicit in scope:

Scala
sourceimport scala.concurrent.duration._
import akka.http.scaladsl.testkit.RouteTestTimeout
import akka.testkit.TestDuration

implicit val timeout: RouteTestTimeout = RouteTestTimeout(5.seconds.dilated)
Java
source@Override
public FiniteDuration awaitDuration() {
    return FiniteDuration.create(5, TimeUnit.SECONDS);
}

Remember to configure the timeout using dilated if you want to account for slow test systems.

Testing Actor integration

The ScalatestRouteTest still provides a Classic ActorSystem, so if you are not using the Classic API you will need to adapt it:

Scala
sourceimport scala.concurrent.duration._
import scala.util.{ Failure, Success }

import akka.{ actor => untyped }
import akka.actor.testkit.typed.scaladsl.TestProbe
import akka.actor.typed.{ ActorRef, ActorSystem, Scheduler }
import akka.actor.typed.scaladsl.AskPattern._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.testkit.ScalatestRouteTest
import akka.util.Timeout

import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

object RouteUnderTest {
  case class Ping(replyTo: ActorRef[String])

  // Your route under test, scheduler is only needed as ask is used
  def route(someActor: ActorRef[Ping])(implicit scheduler: Scheduler, timeout: Timeout) = get {
    path("ping") {
      complete(someActor ? Ping.apply)
    }
  }
}

class TestKitWithActorSpec extends AnyWordSpec with Matchers with ScalatestRouteTest {
  import RouteUnderTest._

  // This test does not use the classic APIs,
  // so it needs to adapt the system:
  import akka.actor.typed.scaladsl.adapter._
  implicit val typedSystem: ActorSystem[_] = system.toTyped
  implicit val timeout: Timeout = Timeout(500.milliseconds)
  implicit val scheduler: untyped.Scheduler = system.scheduler

  "The service" should {
    "return a 'PONG!' response for GET requests to /ping" in {
      val probe = TestProbe[Ping]()
      val test = Get("/ping") ~> RouteUnderTest.route(probe.ref)
      val ping = probe.expectMessageType[Ping]
      ping.replyTo ! "PONG!"
      test ~> check {
        responseAs[String] shouldEqual "PONG!"
      }
    }
  }
}
Java
sourceimport akka.actor.testkit.typed.javadsl.TestProbe;
import akka.actor.typed.ActorSystem;
import akka.actor.typed.javadsl.Adapter;
import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.testkit.JUnitRouteTest;

import akka.http.javadsl.testkit.TestRoute;
import akka.http.javadsl.testkit.TestRouteResult;
import org.junit.Test;

public class TestKitWithActorTest extends JUnitRouteTest {

    @Test
    public void returnPongForGetPing() {
        // This test does not use the classic APIs,
        // so it needs to adapt the system:
        ActorSystem<Void> system = Adapter.toTyped(system());

        TestProbe<MyAppWithActor.Ping> probe = TestProbe.create(system);
        TestRoute testRoute = testRoute(new MyAppWithActor().createRoute(probe.getRef(), system.scheduler()));

        TestRouteResult result = testRoute.run(HttpRequest.GET("/ping"));
        MyAppWithActor.Ping ping = probe.expectMessageClass(MyAppWithActor.Ping.class);
        ping.replyTo.tell("PONG!");
        result.assertEntity("PONG!");
    }
}

Integration Testing Routes

Use ~!> to test a route running in full HTTP server mode:

REQUEST ~!> ROUTE ~> check {
  ASSERTIONS
}

Certain routes can only be tested with ~!>, for example routes that use the withRequestTimeout directive.

Note

Using ~!> adds considerable extra overhead since each test will start a server and bind to a port so use it only when necessary.

Examples

A great pool of examples are the tests for all the predefined directives in Akka HTTP. They can be found here.

Found an error in this documentation? The source code for this page can be found here. Please feel free to edit and contribute a pull request.