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

Helio

06/16/2022, 7:09 AM
Hello team, While upgrading another of our projects from Ktor V1 to Ktor V2, we noticed that a couple of our integration tests were failing... After doing an investigation and comparing the behaviour between Ktor V1 and Ktor V2 we noticed that some of the mockks are no longer working as expected. I wonder if this is an issue with Ktor or if there was any behaviour change that I couldn’t find. Apparently the
testApplication
starts a separate Classloader where the mocked classes/objects are not visible. Is this expected?
The project is quite large. I tried to synthesise the problem within one screenshot. I’m happy to share the snippets in text if you prefer.
a

Aleksei Tirman [JB]

06/16/2022, 7:30 AM
Could you please tell me what is
ArtifactoryClient
and how it’s related to Ktor?
h

Helio

06/16/2022, 7:31 AM
Sure. Let me share what ArtifactoryClient is.
Copy code
class ArtifactoryClient(private val adminToken: String) {
    companion object {
        private const val EXPIRY_TIME = 30L
        private val cache =
            CacheBuilder.newBuilder().expireAfterWrite(Duration.ofMinutes(EXPIRY_TIME))
                .build<String, ArtifactoryClient>()
        private const val cacheKey = "expiryObjectKey"
        private val mutex = Mutex()

        suspend fun getClient(): ArtifactoryClient {
            val client = cache.getIfPresent(cacheKey)
            if (client == null) {
                mutex.withLock {
                    return cache.get(cacheKey) { runBlocking { generateClient() } }
                }
            } else {
                return client
            }
        }

        private suspend fun generateClient(): ArtifactoryClient {
            val tokenatorToken = TokenatorClient.getAdminAccessToken()
            return ArtifactoryClient(tokenatorToken.token)
        }
    }

    private val artifactoryUrlString = "<https://artifactoryUrl>"
    private val artifactoryUrl = URLBuilder(artifactoryUrlString).build()

    private val client = HttpClient(CIO) {
        expectSuccess = true
        install(ContentNegotiation) {
            json(Json {
                ignoreUnknownKeys = true
            })
        }

        defaultRequest {
            url {
                protocol = artifactoryUrl.protocol
                port = artifactoryUrl.port
                host = artifactoryUrl.host
            }
            headers {
                append("Authorization", "Bearer $adminToken")
            }
        }
    }

    suspend fun getItemPermissions(repoKey: String, itemPath: String): ArtifactoryItemPermissions {
        return client.get {
            url { appendPathSegments("api", "storage", repoKey, itemPath) }
            parameter("permissions",  "")
        }.body()
    }
}
ArtifactoryClient is a class that contains the
Ktor HttpClient
we use to forward the requests. In our Class, we do have a companion object that has the
getClient()
that will return the
HttpClient
if it already exists and it is cached, or it will use another Ktor HttpClient (
TokenatorClient
) To send a request to another service to get a token and assign it to the HttpClient of Artifactory client.
a

Aleksei Tirman [JB]

06/16/2022, 7:36 AM
As I can see, the
getClient()
method, that you mock, returns an instance of
ArtifactoryClient
and not the
HttpClient
.
Correct me if I’m wrong.
h

Helio

06/16/2022, 7:39 AM
No, I think you are right. And I reckon what it returns is correct. Because all we wanted to mock is:
Copy code
coEvery { ArtifactoryClient.getClient() } returns artifactoryClient
What we are actually trying to understand is if there was any change in Ktor V2 that perhaps could’ve affected this from being mocked? I’m not sure if I missed anything in the documentation.
Our ArtifactoryClient is meant to be mocked with
Copy code
private val artifactoryClient = mockk<ArtifactoryClient>()
🤔
a

Aleksei Tirman [JB]

06/16/2022, 7:41 AM
So you want to say that because of a Ktor’s code in the
ArtifactoryClient
class mocking is failed?
h

Helio

06/16/2022, 7:43 AM
Not really. What I’m trying to say is that after bumping our project to
Ktor 2.0.2
, the method that was previously being
ArtifactoryClient.getClient()
is no longer identified as “mocked” when running the integration tests. Again, I’m not saying that it is an issue with Ktor, I’m honestly just trying to understand if there was any change around the versions that could’ve triggered this behaviour change.
For example, if you look in the Screenshot for ArtifactoryPermissionService, what we would be expecting is that in line
43
the itemPermissions would be the return from
itemPermissions
in
setUpMockArtifactory()
. But instead, the tests now are trying to access the real value of
ArtifactoryClient.getClient().getItemPermissons(…m…)
During our investigation we suspected that the
testApplication
starts a separate ClassLoader where the mocked classes/objects are not visible.
a

Aleksei Tirman [JB]

06/16/2022, 7:49 AM
During our investigation we suspected that the
testApplication
starts a separate ClassLoader where the mocked classes/objects are not visible.
That’s true because the
development
mode is on in the test environment that enables auto reloading of classes. Probably your problem is related to https://youtrack.jetbrains.com/issue/KTOR-4164.
Could you please try to reproduce your problem without
testApplication
?
Unfortunately, the
development
mode cannot be switched off there.
h

Helio

06/16/2022, 7:54 AM
Would you be able to kindly share an example on how I could make a request to one of my Routes from my test without the
testApplication
?
I indeed tried to have a look at the developmentMode, but no lucky there
a

Aleksei Tirman [JB]

06/16/2022, 8:09 AM
Here is an example:
Copy code
@Test
fun test() {
    val engine = TestApplicationEngine(createTestEnvironment {
        developmentMode = false
        module {
            routing {
                get("/") {
                    call.respondText { "OK" }
                }
            }
        }
    })

    assertFalse(engine.environment.developmentMode)

    engine.start(wait = false)

    val body = runBlocking {
        engine.client.get("/").bodyAsText()
    }
    assertEquals("OK", body)

    engine.stop()
}
🙏 1
👀 1
h

Helio

06/16/2022, 8:14 AM
IIUC, this is what you would expect me my test to be, right?
Copy code
@Test
    fun sendEventsToStreamHub() {
        val respBody = "{\"eventId\":\"94a16a71-4525-40f9-84e1-7178b05a8851\",\"ingestionTime\":\"2021-06-07T01:22:44.920Z\"}"
        val streamHubClient = givenStreamHubResponse(HttpStatusCode.OK, respBody)
        mockkObject(StreamHubClient)
        every { StreamHubClient.getHttpClient() } returns streamHubClient
        setUpMockArtifactory()
        val engine = TestApplicationEngine(createTestEnvironment {
            developmentMode = false
            module {
                routing {
                    post("/permissions/ccm") {
                        runCatching {
                            ArtifactoryPermissionService.sendCcmPermissionsToStreamHub()
                        }.onSuccess {
                            call.respond(HttpStatusCode.OK)
                        }
                    }
                }
            }
        })
        val response = runBlocking {
            <http://engine.client.post|engine.client.post>("/permissions/ccm")
        }
        assertEquals(HttpStatusCode.OK, response.status)
    }
I honestly think that the test progressed a little bit.
Actually, I think that solved the issue.
Do you mind to kindly explain what was the rational behind what you did, please? I would love to understand what we’ve done here.
Firstly, I would like to express my appreciation for your help into this. Honestly, really appreciate it.
a

Aleksei Tirman [JB]

06/16/2022, 8:33 AM
IIUC, this is what you would expect me my test to be, right?
I don’t know. It depends on what behavior do you want to test.
Do you mind to kindly explain what was the rational behind what you did, please? I would love to understand what we’ve done here.
I just use the
TestApplicationEngine
class directly (the
testApplication
function uses it under the hood).
🙌🏽 1