Skip to content

Commit a788677

Browse files
authored
use ktor mock client instead of okhttp mockwebserver (#99)
1 parent b716347 commit a788677

File tree

5 files changed

+59
-77
lines changed

5 files changed

+59
-77
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ kotlin {
4343
implementation(kotlin("test"))
4444
implementation(kotlin("reflect"))
4545
implementation(libs.kotlinx.coroutines.test)
46-
implementation(libs.okhttp.mockwebserver)
46+
implementation(libs.ktor.client.mock)
4747
}
4848
}
4949
}

gradle/libs.versions.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@ tool-prettier = "3.5.3"
1919
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test" }
2020
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
2121
ktorfit = { module = "de.jensklingenberg.ktorfit:ktorfit-lib", version.ref = "gradle-ktorfit" }
22+
ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
2223
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
2324
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
2425

25-
# Deprecated
26-
okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version = "3.14.8" }
27-
2826
[plugins]
2927
dokka = { id = "org.jetbrains.dokka", version.ref = "gradle-dokka" }
3028
jgitver = { id = "fr.brouillard.oss.gradle.jgitver", version.ref = "gradle-jgitver" }

src/jvmMain/kotlin/dev/sargunv/pokekotlin/client/PokeApiClient.kt

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,28 @@ package dev.sargunv.pokekotlin.client
33
import de.jensklingenberg.ktorfit.Ktorfit.Builder
44
import io.ktor.client.HttpClient
55
import io.ktor.client.HttpClientConfig
6+
import io.ktor.client.engine.HttpClientEngine
67
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
78
import io.ktor.serialization.kotlinx.json.json
89

910
class PokeApiClient(
1011
baseUrl: String = "https://pokeapi.co/api/v2/",
12+
engine: HttpClientEngine? = null,
1113
configure: HttpClientConfig<*>.() -> Unit = {},
1214
) :
13-
PokeApi by (Builder()
14-
.apply {
15-
baseUrl(baseUrl)
16-
httpClient(
17-
HttpClient {
18-
install(ContentNegotiation) { json(PokeApiJson) }
19-
configure()
20-
}
21-
)
15+
PokeApi by (run {
16+
fun HttpClientConfig<*>.fullyConfigure() {
17+
install(ContentNegotiation) { json(PokeApiJson) }
18+
configure()
2219
}
23-
.build()
24-
.createPokeApi())
20+
Builder()
21+
.apply {
22+
baseUrl(baseUrl)
23+
httpClient(
24+
if (engine != null) HttpClient(engine) { fullyConfigure() }
25+
else HttpClient { fullyConfigure() }
26+
)
27+
}
28+
.build()
29+
.createPokeApi()
30+
})

src/jvmTest/kotlin/dev/sargunv/pokekotlin/test/EndpointTest.kt

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,23 @@ package dev.sargunv.pokekotlin.test
22

33
import dev.sargunv.pokekotlin.client.PokeApi
44
import dev.sargunv.pokekotlin.client.PokeApiJson
5+
import io.ktor.client.HttpClient
6+
import io.ktor.client.request.get
7+
import io.ktor.client.statement.bodyAsText
58
import kotlin.collections.HashMap
69
import kotlin.reflect.full.declaredMemberFunctions
710
import kotlin.test.Test
811
import kotlin.test.assertEquals
9-
import okhttp3.OkHttpClient
10-
import okhttp3.Request
12+
import kotlinx.coroutines.test.runTest
1113

1214
class EndpointTest {
1315

14-
private val httpClient = OkHttpClient()
16+
private val httpClient = HttpClient()
1517

1618
@Test
17-
fun checkAllEndpoints() {
19+
fun checkAllEndpoints() = runTest {
1820
// call the mock API to get a list of resource endpoints
19-
20-
val json =
21-
httpClient
22-
.newCall(Request.Builder().get().url(MockServer.url).build())
23-
.execute()
24-
.body()!!
25-
.string()
21+
val json = httpClient.get("https://pokeapi.co/api/v2/").bodyAsText()
2622

2723
// parse the expected resources using the list
2824
val expectedSingleResources =
@@ -39,19 +35,16 @@ class EndpointTest {
3935
expectedSingleResources.map { it + "List" }.toSet() + "PokemonEncounterList"
4036

4137
// use reflection to determine the actual resources in the client
42-
4338
val actualResources =
4439
PokeApi::class
4540
.declaredMemberFunctions
4641
.map { it.name.removePrefix("get") }
4742
.groupBy { it.endsWith("List") }
4843

4944
val actualSingleResources = actualResources.getValue(false).toSet()
50-
5145
val actualListResources = actualResources.getValue(true).toSet()
5246

5347
// make sure the resources in the client match the ones in the API
54-
5548
assertEquals(expectedSingleResources.sorted(), actualSingleResources.sorted())
5649
assertEquals(expectedListResources.sorted(), actualListResources.sorted())
5750
}

src/jvmTest/kotlin/dev/sargunv/pokekotlin/test/MockServer.kt

Lines changed: 33 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,64 +2,49 @@ package dev.sargunv.pokekotlin.test
22

33
import dev.sargunv.pokekotlin.client.PokeApiClient
44
import dev.sargunv.pokekotlin.client.PokeApiJson
5+
import io.ktor.client.engine.mock.MockEngine
6+
import io.ktor.client.engine.mock.MockRequestHandleScope
7+
import io.ktor.client.engine.mock.respond
8+
import io.ktor.client.engine.mock.respondError
9+
import io.ktor.client.request.HttpRequestData
10+
import io.ktor.client.request.HttpResponseData
11+
import io.ktor.http.HttpStatusCode
12+
import io.ktor.http.headersOf
513
import java.io.File
614
import java.io.FileReader
7-
import java.nio.charset.Charset
815
import java.nio.file.Paths
9-
import java.util.logging.Level
10-
import java.util.logging.LogManager
1116
import kotlinx.serialization.json.JsonObject
1217
import kotlinx.serialization.json.JsonPrimitive
1318
import kotlinx.serialization.json.buildJsonArray
1419
import kotlinx.serialization.json.buildJsonObject
1520
import kotlinx.serialization.json.jsonArray
16-
import okhttp3.mockwebserver.Dispatcher
17-
import okhttp3.mockwebserver.MockResponse
18-
import okhttp3.mockwebserver.MockWebServer
19-
import okhttp3.mockwebserver.RecordedRequest
20-
import okio.Buffer
2121

2222
object MockServer {
23+
val mockEngine = MockEngine { request -> dispatch(request) }
24+
val client = PokeApiClient(engine = mockEngine)
25+
26+
private val sampleArchivePath = Paths.get(MockServer::class.java.getResource("/data")!!.toURI())
27+
28+
private fun limit(text: String, limit: Int): String {
29+
val fullObj = PokeApiJson.decodeFromString<JsonObject>(text)
30+
val fullResults = fullObj["results"]!!.jsonArray
31+
val newResults = buildJsonArray { fullResults.take(limit).forEach { add(it) } }
32+
val newObj = buildJsonObject {
33+
fullObj.entries.forEach { (key, value) -> put(key = key, element = value) }
34+
put(key = "results", element = newResults)
35+
if (fullResults.size > limit) put(key = "next", element = JsonPrimitive("DUMMY"))
36+
}
37+
return PokeApiJson.encodeToString(newObj)
38+
}
2339

24-
private val server = MockWebServer()
25-
26-
val url = server.url("/api/v2/")!!
27-
val client = PokeApiClient(url.url().toString())
28-
29-
init {
30-
// disable MockWebServer logging
31-
LogManager.getLogManager().getLogger(MockWebServer::class.qualifiedName).level = Level.OFF
32-
33-
// get the path to the sample API responses archive
34-
val sampleArchivePath = Paths.get(MockServer::class.java.getResource("/data")!!.toURI())
35-
36-
// set up the dispatcher to use files in the archive as the mock responses
37-
server.dispatcher =
38-
object : Dispatcher() {
39-
private fun limit(text: String, limit: Int): String {
40-
val fullObj = PokeApiJson.decodeFromString<JsonObject>(text)
41-
val fullResults = fullObj["results"]!!.jsonArray
42-
val newResults = buildJsonArray { fullResults.take(limit).forEach { add(it) } }
43-
val newObj = buildJsonObject {
44-
fullObj.entries.forEach { (key, value) -> put(key, value) }
45-
put("results", newResults)
46-
if (fullResults.size > limit) put("next", JsonPrimitive("DUMMY"))
47-
}
48-
return PokeApiJson.encodeToString(newObj)
49-
}
50-
51-
override fun dispatch(request: RecordedRequest): MockResponse {
52-
val basePath = request.path.dropLastWhile { it != '/' }
53-
val limit = server.url(request.path).queryParameter("limit")?.toInt()
54-
val file = File(sampleArchivePath.toString() + basePath + "index.json")
55-
return if (file.exists()) {
56-
var text = FileReader(file).use { it.readText() }
57-
if (limit != null) text = limit(text, limit)
58-
MockResponse()
59-
.setHeader("content-type", "application/json")
60-
.setBody(Buffer().writeString(text, Charset.defaultCharset()))
61-
} else MockResponse().setResponseCode(404)
62-
}
63-
}
40+
private fun MockRequestHandleScope.dispatch(request: HttpRequestData): HttpResponseData {
41+
val basePath = request.url.encodedPath.dropLastWhile { it != '/' }
42+
val limit = request.url.parameters["limit"]?.toInt()
43+
val file = File(sampleArchivePath.toString() + basePath + "index.json")
44+
return if (file.exists()) {
45+
val text = FileReader(file).use { it.readText() }
46+
val content = if (limit != null) limit(text, limit) else text
47+
respond(content = content, headers = headersOf("content-type", "application/json"))
48+
} else respondError(HttpStatusCode.NotFound)
6449
}
6550
}

0 commit comments

Comments
 (0)