1. Overview
In this tutorial, we’re going to explore how the trait ScalatestRouteTest offered by akka-http-testkit can help us test routes in our Akka HTTP applications. We’ll start by creating some example routes. Then, we can go through how to call those routes and how we can test the responses from them.
To follow along with this article, a basic understanding of routes in Akka HTTP is essential. Our article Introduction to Akka HTTP in Scala is a good primer on the topic.
2. Setup
To be able to test our Akka HTTP applications, we need to include akka-http-testkit to our list of dependencies in build.sbt (We should already have akka-http included):
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-http-testkit" % "10.5.0" % Test
)
Since the testkit is only required for testing, we need to make sure to add Test at the end, to ensure it’s not included in the built jar.
3. Creating Routes to Test
Before we start writing tests, let’s create some routes. We’ll create routes that use a range of headers, query parameters, and request bodies, giving us a good variety of examples test:
val routes =
get {
concat(
path("hello") {
complete("Hello World!")
},
path("hello-name") {
parameters(Symbol("name").as[String]) {
name => {
complete(s"Hello $name")
}
}
},
path("hello-user") {
headerValueByName("X-User-Id") { userId =>
if(userId== "123") {
complete("Welcome user")
} else {
complete(StatusCodes.Unauthorized, "Incorrect user")
}
}
}
)
} ~
post {
path("hello-name") {
entity(as[String]) { name =>
complete(s"Hello $name")
}
}
}
In this snippet, we’ve defined three GET endpoints and one POST endpoint.
The /hello endpoint returns a static String.
Then, /hello-name takes a name as a query parameter and returns it as part of the response.
Next, we have /hello-user that will return 200 if the correct value is present in the X-User-Id header, or 401 if it’s not the expected value.
Finally, we have /hello-name, which responds by saying hello to the name provided in the request body.
4. Calling a Route Using ScalatestRouteTest
Testkit gives us the ability to call a route in our tests. This can be done by calling a function corresponding to the HTTP method of the endpoint such as Get, Put, or Post. Then, we provide it an argument of the path to our endpoint.
For our example, we have a GET endpoint at /hello, so to call it from our tests, we’d use Get(“/hello”). From there, we pass the Route object we are testing, where the endpoint is defined:
Get("/hello") ~> routes
4.1. Providing Query Parameters
Calling an endpoint with a query parameter is simple enough. We just add it to the end of the path for the endpoint:
Get("/hello-name?name=John") ~> routes
4.2. Providing Headers
To provide a header to an endpoint, we need to call addHeader, with the key and value of the header we want to provide:
Get("/hello-user") ~> addHeader("X-User-Id", "123") ~> routes
Additionally, we can also achieve the same outcome by providing the RawHeader object to the request:
Get("/hello-user") ~> RawHeader("X-User-Id", "123") ~> routes
If we need to add multiple headers to our request. We can use the handy addHeaders function, which takes one or more RawHeader objects listed in the arguments:
Get("/hello-user") ~> addHeaders(RawHeader("X-User-Id", "123"), RawHeader("accepts", "*/*")) ~> routes
4.3. Providing a Request Body
Testkit also allows us to test POST methods, which make requests including a body. We need to create a new RequestBuilder and then call the apply function, providing the path to the endpoint and the body of the request.
Let’s make a POST request to our /hello-name endpoint, providing the name “Sarah” in the body of the request:
new RequestBuilder(HttpMethods.POST)("/hello-name", "Sarah") ~> routes
5. Testing Responses
Now we know how to call different types of requests in our tests. Let’s start looking at how to test the responses from them.
Testkit provides a function called check, which we can use after calling our route. check opens up the response, giving us access to all the parts of it we’d want to test. To use check, we add it onto the end of the requests we made in the previous sections:
Get("/hello") ~> routes ~> check
5.1. Testing Response Status
Now that we’ve called check, we have access to the status code of the response simply through the value of status, which is in scope in the code block after the call to check. To test the status code, we can compare it to any of the StatusCodes that Akka HTTP provides.
Let’s call our first route and check the status of the response:
Get("/hello") ~> routes ~> check {
status shouldEqual StatusCodes.OK
}
If we want to test any other status code from our endpoint, all the status codes are available on the StatusCodes object. These include StatusCodes.NoContent, StatusCodes.NotFound, and StatusCodes.InternalServerError, among others.
For example, if we were to call the hello-user route, providing an X-User-Id header of 456 when it’s expecting 123, we should see a status of StatusCodes.Unauthorized:
Get("/hello-user") ~> addHeader("X-User-Id", "456") ~> routes ~> check {
status shouldEqual StatusCodes.Unauthorized
}
5.2. Testing the Response Body
check also provides us with access to the response entity of our request. To do this, we use the responseAs function, providing a type we want to cast the response to:
responseAs[String] shouldBe "Hello World!"
While this is a very powerful tool in akka-http-testkit, responseAs is not type-safe, as it casts the response to the type provided. Beware that depending on the response, the tests may fail with an exception.
Now, let’s put what we’ve learned in the last two sections together to make a full test for one of our example routes:
Get("/hello") ~> routes ~> check {
status shouldEqual StatusCodes.OK
responseAs[String] shouldBe "Hello World!"
}
5.3. Other Response Values From check
There are many other parts of a response that we can test using check, such as headers and contentType. Let’s create another test exploring some of those parts:
Get("/hello-name?name=John") ~> routes ~> check {
status shouldEqual StatusCodes.OK
responseAs[String] shouldBe "Hello John"
headers.length shouldBe 0
contentType shouldBe ContentTypes.`text/plain(UTF-8)`
}
As we can see, check really opens up the response, giving us the ability to test every aspect of it.
6. Conclusion
In this article, we went over the tools that ScalatestRouteTest provides to thoroughly test endpoints in our Scala Akka HTTP applications.
We created a variety of endpoints to practice our testing. Then, we learned how to call the routes we defined from our unit tests. Finally, we used the check function, which allows us to analyze the response and test all aspects of what’s returned by the endpoints we’re testing.
As always, the sample code used in this tutorial is available over on GitHub.