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
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
<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.
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:
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:
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.
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.
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.
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"
}