Integration Testing with Javalin and Fuel HTTP

As can be seen in our previous blogpost, getting started with Javalin and Gradle is quite straightforward. In my experience it’s good to focus on integration testing early in the project. This allows you to refactor with confidence, get quick feedback, and prevent bugs. When using Javalin, this is no different. This blogpost will dive into a way to easily create integrations tests. We’ll use the latest version of Javalin, Kotlin and Junit 5, plus we’ll use Fuel HTTP to test our endpoint.

What is Fuel HTTP

Fuel HTTP is one of the easiest networking libraries for Kotlin and quite actively developed. Fuel HTTP provides great support for handling standard HTTP calls out of the box. There are also modules which support other frameworks such as GSON, Jackson, RxJava and many more. This makes Fuel HTTP well suited for testing REST endpoints, which is what we’ll test in this blogpost.

Our application overview

The application used in this blogpost is a very small one which just retrieves persons by invoking an HTTP GET call with an id parameters to a person endpoint, such as /api/person/1, where 1 is the id of the person. The result of this will be a JSON structure representing the person, as can be seen below:

{"name":"Yvonne","age":29}

The application builds upon the Getting started with Javalin application in our previous blogpost. If you haven’t read it, now might be a good idea do that. Note that you won’t need any code from the previous blogpost though.

The total solution consists of the following:

  • The Gradle build file to manage our dependencies
  • The application configuration including the routing
  • The person domain, including a controller, a DTO and a repository
  • The actual integration tests

The gradle build file

// build.gradle.kt
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    val kotlinVersion = "1.3.21"
    id("org.jetbrains.kotlin.jvm") version kotlinVersion
    application
}

tasks.withType<Test> {
    useJUnitPlatform()
}

dependencies {
    api("io.javalin:javalin:2.8.0")

    implementation("org.slf4j:slf4j-simple:1.7.26")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8")

    testImplementation("org.junit.jupiter:junit-jupiter:5.4.1")
    testImplementation("com.github.kittinunf.fuel:fuel:2.0.1")
    testImplementation("com.github.kittinunf.fuel:fuel-jackson:2.0.1")
}

repositories {
    jcenter()
}

application {
    // Define the main class for the application
    mainClassName = "ApplicationKt"
}

As you can see in the Kotlin Gradle build file above, we’ve defined our production and test dependencies. Some of the dependencies we want to expose, which are defined as api dependencies. The implementation details are hidden from anyone having a dependency on our application. This is done by using the implementation dependencies. Furthermore, we also have our Fuel and JUnit 5 test as testImplementation dependencies, which are used to execute our integration tests.

Application configuration and routing

// Application.kt
import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.get
import io.javalin.apibuilder.ApiBuilder.path
import person.PersonController
import person.personRepository

fun main() {
    JavalinApp(7000).init()
}

class JavalinApp(private val port: Int) {

    fun init(): Javalin {

        val app = Javalin.create().apply {
            port(port)
            exception(Exception::class.java) { e, _ -> e.printStackTrace() }
        }.start()

        val personController = PersonController(personRepository)

        app.get("/api/person/:id", personController::getPerson)

        return app
    }
}

In the above, we’ve defined our JavalinApp. It’s extracted to a separate class to make it easier to invoke it from the test. The Javalin class is responsible for managing the running application, which includes setting up the controller and the API routes.

Person domain model

// person.Person.kt
package person

import io.javalin.Context

class PersonController(private val data: Map<Int, Person>) {

    fun getPerson(ctx: Context) {
        ctx.pathParam("id").toInt().let {
            data[it]?.let { item ->
                ctx.json(item)
                return
            }
            ctx.status(404)
        }
    }
}

data class Person(val name: String, val age: Int)

// In-memory repository
val personRepository = hashMapOf(
        0 to Person("Dmitry", 37),
        1 to Person("Yvonne", 29),
        2 to Person("Peter", 52)
)

The above code has been merged together into one file. This isn’t necessarily a great idea, but for the purpose of our blogpost, this was easier. It contains a controller which is using our injected repository, a person class, and the in memory repository itself containing our dummy persons.

Integration tests

// PersonIntegrationTest.kt
package person

import JavalinApp
import com.github.kittinunf.fuel.core.FuelManager
import com.github.kittinunf.fuel.httpGet
import com.github.kittinunf.fuel.jackson.responseObject
import io.javalin.Javalin
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.assertEquals

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DisplayName("Person API")
class PersonIntegrationTest  {

    private lateinit var app: Javalin

    @BeforeAll
    fun setUp() {
        app = JavalinApp(8000).init()
        // Inject the base path to no have repeat the whole URL
        FuelManager.instance.basePath = "http://localhost:${app.port()}/"
    }

    @AfterAll
    fun tearDown() {
        // Stops the application when the tests have completed
        app.stop()
    }

    @Test
    fun `should get result for existing person`() {
        // Deconstructs the ResponseResult
        val (_, _, result) = "api/person/0".httpGet().responseObject<Person>()

        // Get the actual value from the result object
        assertEquals(personRepository[0], result.get())
    }

    @Test
    fun `should get error for non-existing person`() {
        val (_, _, result) = "api/person/-1".httpGet().responseObject<Person>()
        // Deconstructs the ResponseResult to get the FuelError
        val (_, error) = result

        assertEquals(404, error!!.response.statusCode)
    }
}

And now, after setting up our production infrastructure, this is our end result. It’s an integration test which is testing two scenarios: a scenario in which there is a result found and a scenario which results into an error.

In the above code, we’re using the synchronous version of Fuel HTTP, which is blocking until there is a result. Then we destructure the result into a separate fields and extract the ones we need.

Note: if anyone know how to get rid of the !! in an elegant way, please leave a comment below.

Executing the test using gradlew test or using your favorite IDE will result in the following test report:

Person API
+-- should get error for non-existing person   [PASSED]
+-- should get result for existing person      [PASSED]

While the example above is quite limited and in no way covers all the aspects of REST endpoint, it hopefully sets you up to start testing Javalin applications. If you have any comments, please leave me a message below!

2 thoughts on “Integration Testing with Javalin and Fuel HTTP

  1. I like it, seems super simple!

    For getting rid of the !!, you can make use of the fact that the result is a sealed class, and write something like:

    val (_, _, result) = “api/person/-1”.httpGet().responseObject()
    when(result) {
    is Result.Failure -> assertEquals(404, result.error.response.statusCode)
    else -> fail(“Expected call to fail”)
    }

    I’m not sure whether it’s worth all the additional typing for such a simple test case, though you do get a nicer error message in the case that it fails.

    1. Hi Jake, thanks for the feedback, thanks for that! I thought of that, but it did complicate the code a bit, and it gives the impression that there are 2 possible options. I also thought about using question marks (?), but it’s also not ideal. Maybe sometimes it’s not so bad to have a few !! in there 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *