https://kotlinlang.org logo
#ktor
Title
# ktor
d

dave08

11/02/2021, 10:00 AM
Is there a simple way to create an HttpClient using TestApplicationEngine without the withTestApplication around each test? I just want to create the client once on test initialization and use it without withTestApplication and handleRequest...
a

Aleksei Tirman [JB]

11/02/2021, 10:16 AM
The
TestApplicationEngine
doesn't use
HttpClient
instance as well as a client's Mock engine doesn't use Ktor server.
d

dave08

11/02/2021, 10:28 AM
This is in the TestApplicationEngine source code:
Copy code
}

    /**
     * An instance of client engine user to be used in [client].
     */
    val engine: HttpClientEngine = TestHttpClientEngine.create { app = this@TestApplicationEngine }

    /**
     * A client instance connected to this test server instance. Only works until engine stop invocation.
     */
    val client: HttpClient = HttpClient(engine)
It seems like that's what it does internally...
It just seems a bit complex over there...
a

Aleksei Tirman [JB]

11/02/2021, 11:02 AM
Indeed. Could you please describe your problem in more detail?
In principle you can do it but it will be bound to specific
TestApplicationEngine
c

christophsturm

11/02/2021, 11:21 AM
this worked for me:
Copy code
val server = TestApplicationEngine(environment = createTestEnvironment {
            module {
...
            }
        }) {}.apply { start() }
d

dave08

11/02/2021, 11:46 AM
And how do you get the client @christophsturm?
@Aleksei Tirman [JB] The main reason why I would like this is to simplify test code... there's just too much boilerplate for each test... it would be so much simpler to have:
Copy code
@Test
fun `it ...`() = runBlocking { 
// Arrange
....

// Act
val response = client....

// Assert
...
}
instead of having to nest everything over and over.
Also, the test code could be more similar to the actual code in the Android (or whatever) app that's requesting...
c

christophsturm

11/02/2021, 11:58 AM
i don’t create a client, i just call handleRequest on the server.
🤕 1
and I do it because i want to do it just once in my test context.
a

Aleksei Tirman [JB]

11/02/2021, 1:32 PM
@dave08 in other words you want to set the server part up once and make requests in each test, correct?
Otherwise, it's already works as you described. Here is an example:
Copy code
@Test
fun test() = withTestApplication {
    // Arrange
    with(application) {
        routing {
            get("/") {
                call.respondText { "hello" }
            }
        }
    }
    // Act
    val response = handleRequest(HttpMethod.Get, "/").response

    // Assert
    assertEquals(HttpStatusCode.OK, response.status())
    assertEquals("hello", response.content)
}
d

dave08

11/02/2021, 1:53 PM
in other words you want to set the server part up once and make requests in each test, correct?
yes. What you called Arrange, isn't arrange for me, it's the SUT (subject under test), that doesn't change in the arrange step --- arrange has more to do with db setup and request inputs that get sent to the SUT or are the state the SUT is in... @Aleksei Tirman [JB]
I think
handleRequest
doesn't have all the features of a regular HttpClient... and might not have all required plugins installed (at least the ones that make it similar to the ones the android app is using...)? So it seems to be harder to use... that's apart from the first problem I had of boilerplate code...
It would have been nice to be able to do something like:
Copy code
// In main module
fun Application.appModule() {
  install(...) { }

  route(...) { ... }
}
In test class:
Copy code
val client = HttpClient(TestAppEngine(::appModule))
r

Rustam Siniukov

11/02/2021, 2:25 PM
There is a design ticket for new testing API that we want to implement in 2.0.0 https://youtrack.jetbrains.com/issue/KTOR-971 Please take a look and comment if this will cover your needs
a

Aleksei Tirman [JB]

11/02/2021, 2:26 PM
You can create an instance of
TestApplicationEngine
manually, assign the client to a test class property, and make requests in each test to the same server instance. Here is an example:
Copy code
import io.ktor.application.*
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.testing.*
import kotlinx.coroutines.*
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals

fun Application.module() {
    routing {
        get("/1") {
            call.respondText { "hello" }
        }

        get("/2") {
            call.respondText { "world" }
        }
    }
}

class SimpleTest {
    private lateinit var engine: TestApplicationEngine
    private lateinit var client: HttpClient

    @BeforeTest
    fun arrange() {
        engine = TestApplicationEngine(createTestEnvironment())
        engine.application.module()
        client = engine.client
        engine.start()
    }

    @AfterTest
    fun stopServer() {
        engine.stop(0L, 0L)
    }

    @Test
    fun test1() = runBlocking {
        val response = client.get<String>("/1")
        assertEquals("hello", response)
    }

    @Test
    fun test2() = runBlocking {
        val response = client.get<String>("/2")
        assertEquals("world", response)
    }
}
d

dave08

11/02/2021, 2:33 PM
I would have preferred the opposite... instead of having
client = engine.client
where the client is being created by by the TestEngine, to use the TestEngine as the client's Engine (like instead of CIO, or OkHttp...), even in that ticket it seems like the proposition is going in the same direction in that sense... that way you stick to SRP (and dependency injection is easier... the same tests could be used with a real server running on a port, or the fake server...)
a

Aleksei Tirman [JB]

11/02/2021, 2:36 PM
I understand but it's not possible right now that's why I can only suggest the solution above.
d

dave08

11/02/2021, 2:37 PM
Ok, I guess that's better than nothing... but what about adding plugins to the client? Is that currently possible?
a

Aleksei Tirman [JB]

11/02/2021, 2:45 PM
Actually, you can create an instance of
HttpClient
with a test server engine. In that case you can install any plugins for the client.
Copy code
import io.ktor.application.*
import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.testing.*
import io.ktor.server.testing.client.*
import kotlinx.coroutines.*
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals

fun Application.module() {
    routing {
        get("/1") {
            call.respondText { "hello" }
        }

        get("/2") {
            call.respondText { "world" }
        }

        get("/3") {
            call.respondText { call.request.headers["custom"]!! }
        }
    }
}

class SimpleTest {
    private lateinit var engine: TestApplicationEngine
    private lateinit var client: HttpClient

    @BeforeTest
    fun arrange() {
        engine = TestApplicationEngine(createTestEnvironment())
        engine.application.module()
        val clientEngine = TestHttpClientEngine.create { app = engine }
        client = HttpClient(clientEngine) {
            install(DefaultRequest) {
                header("custom", "value")
            }
        }
        engine.start()
    }

    @AfterTest
    fun stopServer() {
        engine.stop(0L, 0L)
    }

    @Test
    fun test1() = runBlocking {
        val response = client.get<String>("/1")
        assertEquals("hello", response)
    }

    @Test
    fun test2() = runBlocking {
        val response = client.get<String>("/2")
        assertEquals("world", response)
    }

    @Test
    fun testPlugin() = runBlocking {
        val response = client.get<String>("/3")
        assertEquals("value", response)
    }
}
d

dave08

11/02/2021, 2:50 PM
That's great 👍🏼! Just what I wanted! Just wondering... would it be compatible with ktor client 2.0? I'm not quite ready to use it on the server side, but to get used to the new api in unit tests might be a good way to get started with it...
Does the engine have to be started after it's passed to the client @Aleksei Tirman [JB]?
If not, I guess a start on a Kotest extension would be easy:
Copy code
class KtorExtension(val module: Application.() -> Unit) : TestListener {
    lateinit var appEngine: TestApplicationEngine

    lateinit var clientEngine: HttpClientEngine


    override suspend fun beforeTest(testCase: TestCase) {
        appEngine = TestApplicationEngine(createTestEnvironment())
            .also { it.application.module() }

        clientEngine = TestHttpClientEngine.create { app = appEngine }

        appEngine.start()
    }

    override suspend fun afterTest(testCase: TestCase, result: TestResult) {
        appEngine.stop(0L, 0L)
    }
}
a

Aleksei Tirman [JB]

11/02/2021, 6:31 PM
@dave08
Does the engine have to be started after it's passed to the client
No, you can start an engine before an assignment
Just wondering... would it be compatible with ktor client 2.0?
Yes, it's compatible with Ktor 2.0.0. You just need to adjust imports of packages and API for getting the body of a response.
184 Views