Sayantan Ghosh
Sayantan Ghosh

Reputation: 338

How to mock HttpClient and HttpRequest for unit test in kotlin?

I have built a spring boot application that has a class called FileDetails.kt. The method that I am trying to test, in the class, is in the following format.

getFileDetails(auth: String, id: String): FileModel {
    val url = URI.create(“http://localhost:8080/$id”)
    val client = HttpClient.newBuilder().build()
    val request = HttpRequest.newBuilder().uri(url).header(“Authorization”, auth).build()
    val response = client.send(request, HttpResponse.BodyHandlers.ofString())
    return objectMapper.readValue(response.body(), object: TypeReference<FileModel>() {})}

I am writing a unit test to mock that when the response code is 200, we get a FileModel response. This is what I have done so far but unsure how to proceed.

@Test fun `test get file details method`() {
val response200 = Mockito.mock(HttpResponse::class.java)
val mockRequest = Mockito.mock(HttpRequest::class.java)
// not sure how to return mockRequest and response200}

I am fairly new this and would like to know if it is possible to mock these responses and how to go about that.

Upvotes: 2

Views: 4214

Answers (2)

user598656
user598656

Reputation: 462

You can combine the static mocking and capture capabilities of mockk to validate e.g. a POST request payload etc. along these lines:

import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpRequest.BodyPublishers
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets

    val httpClient = mockk<HttpClient>()
    val httpResponse = mockk<HttpResponse<String>>()

    every { httpResponse.body() } returns "ignored"
    every { httpResponse.statusCode() } returns 200

    val httpRequestCapturingSlot = slot<HttpRequest>()

    every {
        httpClient.send(
            capture(httpRequestCapturingSlot),
            HttpResponse.BodyHandlers.ofString()
        )
    } returns httpResponse

    val expectedRequestJson = Gson().toJson(
        <your expected payload object>
    )

    mockkStatic(BodyPublishers::class)
    val postBodyCapturingSlot = slot<String>()
    every { BodyPublishers.ofString(capture(postBodyCapturingSlot)) } returns BodyPublishers.ofString(
        "stand-in only",
        StandardCharsets.UTF_8
    )

    // make SUT call resulting in HttpRequest

    val contentHeader = "Content-Type" to listOf("application/json")

    val httpRequest = httpRequestCapturingSlot.captured

    httpRequest.method() shouldBe "POST"
    httpRequest.uri().toString() shouldBe <expected uri>
    httpRequest.headers().map() shouldContain contentHeader

    postBodyCapturingSlot.captured shouldBe expectedRequestJson

Upvotes: 1

AndrewL
AndrewL

Reputation: 3390

You are going about this the wrong way.

If your aim is just to mock getFileDetails() then this is easy enough and you don't need to be bothered with HttpResponse at all. Firstly, suppose your fetcher was in a class like this:

class Fetcher {

    fun getFileDetails(auth: String, id: String): FileModel {
        val url = URI.create("http://localhost:8080/$id")
        val client = HttpClient.newBuilder().build()
        val request = HttpRequest.newBuilder().uri(url).header("Authorization", auth).build()
        return objectMapper.readValue(response.body(), object: TypeReference<FileModel>() {})}
    }

}

you simply write (and apologies I am using https://mockk.io/ not Mockito since Mockk is a mocking library specifically for Kotlin which I know better - but the concepts / approach is the same),

import io.mockk.every
import io.mockk.mockk

val fetcher = mockk<Fetcher>()

every { fetcher.fetchRawResponse(any(), any()) } returns FileModel(...)

If you really wanted to test at a lower level with HttpRequest/Response, you will need to break down getFileDetails (Dependency Injection style). What is it you are trying to test?

  1. that the HTTP request is properly formed?
  2. that the deserialization from String to FileModel is correct?

Suppose it was just (2) you wanted to test - I would keep away from testing any Http stuff and assume that the authors of HttpClient have tested their code properly, and abstract your external calls something like this:

class Fetcher {

    fun fetchRawResponse(auth: String, id: String) : HttpResponse<String> {
        val url = URI.create("http://localhost:8080/$id")
        val client = HttpClient.newBuilder().build()
        val request = HttpRequest.newBuilder().uri(url).header("Authorization", auth).build()
        return client.send(request, HttpResponse.BodyHandlers.ofString())
    }

}

Then the caller can do the objectMapper.readValue (or even go further and don't leak the HttpResponse object out of this method, by returning a plain String)

Supposing you had a Fetcher class like the above, this is how to use mock the response.

import io.mockk.every
import io.mockk.mockk
import io.mockk.verify

    @Test fun `test get file details method`() {
        val jsonResponse: HttpResponse<String> = mockk()
        every { jsonResponse.body() } returns  """
            {"some":"json"}
        """.trimIndent()
        val fetcher = mockk<Fetcher>()
        every { fetcher.fetchRawResponse(any(), any()) } returns jsonResponse
        // now you can call fetcher.fetchRawResponse("abc","def")
    }

Upvotes: 2

Related Questions