Testing

The genhttp-testing package provides an easy way to write component tests for your application using a test framework of your choice. It provides both the ability to host your project in an isolated mode as well as convenience methods to run HTTP requests against your server.

Writing Tests

Add the package to the test dependencies of your project, together with the test framework you prefer - the samples on this page use JUnit and kotlin.test:

Gradle

build.gradle.kts
dependencies {
    testImplementation("org.genhttp:genhttp-testing:11.1.0.3")

    testImplementation(kotlin("test"))
    testImplementation(platform("org.junit:junit-bom:6.1.3"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
    useJUnitPlatform()
}

Maven

pom.xml
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>6.1.3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.genhttp</groupId>
        <artifactId>genhttp-testing</artifactId>
        <version>11.1.0.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-test-junit5</artifactId>
        <version>${kotlin.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

The following code shows how the TestHost can be used to spin up a server instance hosting the functionality of the app to be tested and how to run requests against this instance.

Kotlin
import kotlinx.coroutines.runBlocking

import org.genhttp.modules.functional.*
import org.genhttp.testing.TestHost
import org.genhttp.testing.getContent

import org.junit.jupiter.api.Test

import kotlin.test.assertEquals

class MyTests {

    @Test
    fun testMyApp() = runBlocking<Unit> {
        val app = Inline.create().get("/some/path") { "Hello World" } // setup your app here

        TestHost.run(app).use { runner ->
            val response = runner.getResponse("/some/path")

            assertEquals(200, response.statusCode())
            assertEquals("Hello World", response.getContent())
        }
    }

}

TestHost.run() starts the server on a port of its own, use stops it again at the end of the test. The functions of the test host suspend, so a test runs them in a coroutine - runBlocking is the simplest way to do this. By default, the server is configured with the defaults of the practices module (defaults()) and in development mode; pass defaults = false or development = false to test your app without them.

The framework uses the HTTP client of the JDK (java.net.http) to execute requests, so that the semantics are the same, e.g. when performing POST requests with a body:

Kotlin
import org.genhttp.testing.TestHost

import java.net.http.HttpRequest.BodyPublishers

suspend fun postBody(runner: TestHost) {
    val request = runner.getRequest("/some/path")
        .POST(BodyPublishers.ofString("My Body"))

    val response = runner.getResponse(request)
}

getRequest() returns an HttpRequest.Builder for the given path (and optionally a request method such as "DELETE"), which can be adjusted as needed. Responses are read completely (HttpResponse<ByteArray>), so they do not need to be closed. Requests are sent with a shared client that neither follows redirects nor keeps cookies - pass a client created by TestHost.getClient() to change this, e.g. runner.getResponse(request, TestHost.getClient(followRedirects = true)).

The JDK client refuses to send some headers by default, such as Host, Connection or Content-Length. TestHost allows them by setting the system property jdk.httpclient.allowRestrictedHeaders before it creates its first client. The JDK reads this property only once, so if your tests use the JDK client before the test host, set it for the test JVM instead: tasks.test { jvmArgs("-Djdk.httpclient.allowRestrictedHeaders=host,connection,content-length,expect,upgrade") }.

Choosing an Engine

By default the test host runs on the internal engine. To verify that an application behaves the same on another engine, pass a ServerEngine - the same enum the server reports through Server.serverEngine:

Kotlin
import org.genhttp.api.content.HandlerBuilder
import org.genhttp.api.infrastructure.ServerEngine
import org.genhttp.testing.TestHost

suspend fun runOnNettyEngine(app: HandlerBuilder): TestHost =
    TestHost.run(app, engine = ServerEngine.Netty)

The test host runs on ServerEngine.Internal and ServerEngine.Netty - Kestrel and Ioxide are specific to .NET and not available on the JVM. Keep in mind that the Netty engine runs your handlers on its event loops, where they must not block.

Response Handling

The test framework provides some extension functions to simplify reading typed responses.

Kotlin
import org.genhttp.testing.TestHost
import org.genhttp.testing.getContent
import org.genhttp.testing.getOptionalContent

data class MyType(val id: Int, val name: String)

suspend fun readContent(runner: TestHost) {
    val response = runner.getResponse()

    val text: String = response.getContent()

    val typed = response.getContent<MyType>()

    val typedNullable = response.getOptionalContent<MyType>() // might be null
}

Those functions allow to deserialize all formats supported by genhttp-kt (JSON, XML, YAML, form encoded, Protobuf), chosen by the Content-Type of the response. getOptionalContent() returns null if the server responded with 204 No Content.

Kotlin
import org.genhttp.testing.TestHost
import org.genhttp.testing.getContentHeader
import org.genhttp.testing.getHeader

suspend fun readHeaders(runner: TestHost) {
    val response = runner.getResponse()

    val header = response.getHeader("X-My-Header")
    val contentType = response.getContentHeader("Content-Type")
}

As the JDK client does not distinguish between response and content headers, getContentHeader() is the same as getHeader() - both return null if the header is missing.

Accessing the Live Server

If a test needs the actual URL the server is listening on (e.g. to hand it to another library), use getUrl() (or port for the port only). TestHost.nextPort() returns the next port of the range used by the test infrastructure, in case you need to bind additional resources alongside the server under test. The range starts at 22010 - set the environment variable GENHTTP_TEST_PORT_BASE to another value to run several test suites on the same machine at the same time.

Kotlin
import org.genhttp.testing.TestHost

fun describe(runner: TestHost): String {
    val url = runner.getUrl("/some/path")

    return "Server is listening on port ${runner.port}, the page is available at $url"
}