Skip to content

Commit 7bdd6b9

Browse files
authored
Refactor integration tests (#256)
## Motivation and Context Flaky integration tests ## How Has This Been Tested? `./gradlew check` ## Changes - Use random port for Ktor in tests - Remove unused `TestUtils` utility class. - Replace `runTest` with `runBlocking` in test cases. - Adjust imports and formatting for consistency. - Add `Awaitility` dependency for dynamic server setup. - Update `Duration` usage from milliseconds to seconds. - Minor configuration and code formatting updates. ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [x] Test update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [ ] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [ ] My code follows the repository's style guidelines - [ ] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions -->
1 parent be22708 commit 7bdd6b9

File tree

16 files changed

+174
-195
lines changed

16 files changed

+174
-195
lines changed

gradle/libs.versions.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jreleaser = "1.19.0"
1717
binaryCompatibilityValidatorPlugin = "0.18.1"
1818
slf4j = "2.0.17"
1919
kotest = "5.9.1"
20+
awaitility = "4.3.0"
2021

2122
# Samples
2223
mcp-kotlin = "0.7.0"
@@ -45,11 +46,12 @@ ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", v
4546
ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }
4647

4748
# Testing
49+
awaitility = { group = "org.awaitility", name = "awaitility-kotlin", version.ref = "awaitility" }
50+
kotest-assertions-json = { group = "io.kotest", name = "kotest-assertions-json", version.ref = "kotest" }
4851
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
49-
ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" }
5052
ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" }
53+
ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" }
5154
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
52-
kotest-assertions-json = { group = "io.kotest", name = "kotest-assertions-json", version.ref = "kotest" }
5355

5456
# Samples
5557
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }

kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Protocol.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,10 @@ import kotlinx.serialization.json.JsonObject
3636
import kotlinx.serialization.json.JsonPrimitive
3737
import kotlinx.serialization.json.encodeToJsonElement
3838
import kotlinx.serialization.serializer
39-
import kotlin.collections.get
4039
import kotlin.reflect.KType
4140
import kotlin.reflect.typeOf
4241
import kotlin.time.Duration
43-
import kotlin.time.Duration.Companion.milliseconds
42+
import kotlin.time.Duration.Companion.seconds
4443

4544
private val LOGGER = KotlinLogging.logger { }
4645

@@ -85,7 +84,7 @@ public open class ProtocolOptions(
8584
/**
8685
* The default request timeout.
8786
*/
88-
public val DEFAULT_REQUEST_TIMEOUT: Duration = 60000.milliseconds
87+
public val DEFAULT_REQUEST_TIMEOUT: Duration = 60.seconds
8988

9089
/**
9190
* Options that can be given per request.

kotlin-sdk-test/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ kotlin {
2424
jvmTest {
2525
dependencies {
2626
implementation(kotlin("test-junit5"))
27+
implementation(libs.awaitility)
2728
runtimeOnly(libs.slf4j.simple)
2829
}
2930
}

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
1717
import io.modelcontextprotocol.kotlin.sdk.server.mcp
1818
import kotlinx.coroutines.runBlocking
1919
import kotlinx.coroutines.withTimeout
20+
import org.awaitility.kotlin.await
2021
import org.junit.jupiter.api.AfterEach
2122
import org.junit.jupiter.api.BeforeEach
2223
import kotlin.time.Duration.Companion.seconds
@@ -27,7 +28,7 @@ import io.ktor.server.sse.SSE as ServerSSE
2728
abstract class KotlinTestBase {
2829

2930
protected val host = "localhost"
30-
protected abstract val port: Int
31+
protected var port: Int = 0
3132

3233
protected lateinit var server: Server
3334
protected lateinit var client: Client
@@ -39,6 +40,12 @@ abstract class KotlinTestBase {
3940
@BeforeEach
4041
fun setUp() {
4142
setupServer()
43+
await
44+
.ignoreExceptions()
45+
.until {
46+
port = runBlocking { serverEngine.engine.resolvedConnectors().first().port }
47+
port != 0
48+
}
4249
runBlocking {
4350
setupClient()
4451
}

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptEdgeCasesTest.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import io.modelcontextprotocol.kotlin.sdk.PromptMessage
77
import io.modelcontextprotocol.kotlin.sdk.Role
88
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
99
import io.modelcontextprotocol.kotlin.sdk.TextContent
10-
import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest
10+
import kotlinx.coroutines.Dispatchers
1111
import kotlinx.coroutines.launch
1212
import kotlinx.coroutines.runBlocking
13+
import kotlinx.coroutines.test.runTest
1314
import org.junit.jupiter.api.Test
1415
import org.junit.jupiter.api.assertThrows
1516
import kotlin.test.assertEquals
@@ -18,8 +19,6 @@ import kotlin.test.assertTrue
1819

1920
class PromptEdgeCasesTest : KotlinTestBase() {
2021

21-
override val port = 3008
22-
2322
private val basicPromptName = "basic-prompt"
2423
private val basicPromptDescription = "A basic prompt for testing"
2524

@@ -183,7 +182,7 @@ class PromptEdgeCasesTest : KotlinTestBase() {
183182
}
184183

185184
@Test
186-
fun testBasicPrompt() = runTest {
185+
fun testBasicPrompt() = runBlocking(Dispatchers.IO) {
187186
val testName = "Alice"
188187
val result = client.getPrompt(
189188
GetPromptRequest(
@@ -215,7 +214,7 @@ class PromptEdgeCasesTest : KotlinTestBase() {
215214
}
216215

217216
@Test
218-
fun testComplexPromptWithManyArguments() = runTest {
217+
fun testComplexPromptWithManyArguments() = runBlocking(Dispatchers.IO) {
219218
val arguments = (1..10).associate { i -> "arg$i" to "value$i" }
220219

221220
val result = client.getPrompt(
@@ -253,7 +252,7 @@ class PromptEdgeCasesTest : KotlinTestBase() {
253252
}
254253

255254
@Test
256-
fun testLargePrompt() = runTest {
255+
fun testLargePrompt() = runBlocking(Dispatchers.IO) {
257256
val result = client.getPrompt(
258257
GetPromptRequest(
259258
name = largePromptName,
@@ -275,7 +274,7 @@ class PromptEdgeCasesTest : KotlinTestBase() {
275274
}
276275

277276
@Test
278-
fun testSpecialCharacters() = runTest {
277+
fun testSpecialCharacters() = runBlocking(Dispatchers.IO) {
279278
val result = client.getPrompt(
280279
GetPromptRequest(
281280
name = specialCharsPromptName,

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/PromptIntegrationTest.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import io.modelcontextprotocol.kotlin.sdk.PromptMessageContent
99
import io.modelcontextprotocol.kotlin.sdk.Role
1010
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
1111
import io.modelcontextprotocol.kotlin.sdk.TextContent
12-
import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest
12+
import kotlinx.coroutines.Dispatchers
1313
import kotlinx.coroutines.runBlocking
1414
import org.junit.jupiter.api.Test
1515
import org.junit.jupiter.api.assertThrows
@@ -19,7 +19,6 @@ import kotlin.test.assertTrue
1919

2020
class PromptIntegrationTest : KotlinTestBase() {
2121

22-
override val port = 3004
2322
private val testPromptName = "greeting"
2423
private val testPromptDescription = "A simple greeting prompt"
2524
private val complexPromptName = "multimodal-prompt"
@@ -219,7 +218,7 @@ class PromptIntegrationTest : KotlinTestBase() {
219218
}
220219

221220
@Test
222-
fun testListPrompts() = runTest {
221+
fun testListPrompts() = runBlocking(Dispatchers.IO) {
223222
val result = client.listPrompts()
224223

225224
assertNotNull(result, "List prompts result should not be null")
@@ -247,7 +246,7 @@ class PromptIntegrationTest : KotlinTestBase() {
247246
}
248247

249248
@Test
250-
fun testGetPrompt() = runTest {
249+
fun testGetPrompt() = runBlocking(Dispatchers.IO) {
251250
val testName = "Alice"
252251
val result = client.getPrompt(
253252
GetPromptRequest(
@@ -290,7 +289,7 @@ class PromptIntegrationTest : KotlinTestBase() {
290289
}
291290

292291
@Test
293-
fun testMissingRequiredArguments() = runTest {
292+
fun testMissingRequiredArguments() = runBlocking(Dispatchers.IO) {
294293
val promptsList = client.listPrompts()
295294
assertNotNull(promptsList, "Prompts list should not be null")
296295
val strictPrompt = promptsList.prompts.find { it.name == strictPromptName }
@@ -364,7 +363,7 @@ class PromptIntegrationTest : KotlinTestBase() {
364363
}
365364

366365
@Test
367-
fun testComplexContentTypes() = runTest {
366+
fun testComplexContentTypes() = runBlocking(Dispatchers.IO) {
368367
val topic = "artificial intelligence"
369368
val result = client.getPrompt(
370369
GetPromptRequest(
@@ -418,7 +417,7 @@ class PromptIntegrationTest : KotlinTestBase() {
418417
}
419418

420419
@Test
421-
fun testMultipleMessagesAndRoles() = runTest {
420+
fun testMultipleMessagesAndRoles() = runBlocking(Dispatchers.IO) {
422421
val topic = "climate change"
423422
val result = client.getPrompt(
424423
GetPromptRequest(

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceEdgeCasesTest.kt

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
99
import io.modelcontextprotocol.kotlin.sdk.SubscribeRequest
1010
import io.modelcontextprotocol.kotlin.sdk.TextResourceContents
1111
import io.modelcontextprotocol.kotlin.sdk.UnsubscribeRequest
12-
import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest
12+
import kotlinx.coroutines.Dispatchers
1313
import kotlinx.coroutines.launch
1414
import kotlinx.coroutines.runBlocking
15+
import kotlinx.coroutines.test.runTest
1516
import org.junit.jupiter.api.Test
1617
import org.junit.jupiter.api.assertThrows
1718
import java.util.concurrent.atomic.AtomicBoolean
@@ -21,8 +22,6 @@ import kotlin.test.assertTrue
2122

2223
class ResourceEdgeCasesTest : KotlinTestBase() {
2324

24-
override val port = 3007
25-
2625
private val testResourceUri = "test://example.txt"
2726
private val testResourceName = "Test Resource"
2827
private val testResourceDescription = "A test resource for integration testing"
@@ -129,7 +128,7 @@ class ResourceEdgeCasesTest : KotlinTestBase() {
129128
}
130129

131130
@Test
132-
fun testBinaryResource() = runTest {
131+
fun testBinaryResource() = runBlocking(Dispatchers.IO) {
133132
val result = client.readResource(ReadResourceRequest(uri = binaryResourceUri))
134133

135134
assertNotNull(result, "Read resource result should not be null")
@@ -142,7 +141,7 @@ class ResourceEdgeCasesTest : KotlinTestBase() {
142141
}
143142

144143
@Test
145-
fun testLargeResource() = runTest {
144+
fun testLargeResource() = runBlocking(Dispatchers.IO) {
146145
val result = client.readResource(ReadResourceRequest(uri = largeResourceUri))
147146

148147
assertNotNull(result, "Read resource result should not be null")
@@ -172,7 +171,7 @@ class ResourceEdgeCasesTest : KotlinTestBase() {
172171
}
173172

174173
@Test
175-
fun testDynamicResource() = runTest {
174+
fun testDynamicResource() = runBlocking(Dispatchers.IO) {
176175
val initialResult = client.readResource(ReadResourceRequest(uri = dynamicResourceUri))
177176
assertNotNull(initialResult, "Initial read result should not be null")
178177
val initialContent = (initialResult.contents.firstOrNull() as? TextResourceContents)?.text
@@ -188,7 +187,7 @@ class ResourceEdgeCasesTest : KotlinTestBase() {
188187
}
189188

190189
@Test
191-
fun testResourceAddAndRemove() = runTest {
190+
fun testResourceAddAndRemove() = runBlocking(Dispatchers.IO) {
192191
val initialList = client.listResources()
193192
assertNotNull(initialList, "Initial list result should not be null")
194193
val initialCount = initialList.resources.size
@@ -261,7 +260,7 @@ class ResourceEdgeCasesTest : KotlinTestBase() {
261260

262261
@Test
263262
fun testSubscribeAndUnsubscribe() {
264-
runTest {
263+
runBlocking(Dispatchers.IO) {
265264
val subscribeResult = client.subscribeResource(SubscribeRequest(uri = testResourceUri))
266265
assertNotNull(subscribeResult, "Subscribe result should not be null")
267266

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/ResourceIntegrationTest.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
88
import io.modelcontextprotocol.kotlin.sdk.SubscribeRequest
99
import io.modelcontextprotocol.kotlin.sdk.TextResourceContents
1010
import io.modelcontextprotocol.kotlin.sdk.UnsubscribeRequest
11-
import io.modelcontextprotocol.kotlin.sdk.integration.utils.TestUtils.runTest
11+
import kotlinx.coroutines.Dispatchers
12+
import kotlinx.coroutines.runBlocking
1213
import org.junit.jupiter.api.Test
1314
import kotlin.test.assertEquals
1415
import kotlin.test.assertNotNull
1516
import kotlin.test.assertTrue
1617

1718
class ResourceIntegrationTest : KotlinTestBase() {
1819

19-
override val port = 3005
2020
private val testResourceUri = "test://example.txt"
2121
private val testResourceName = "Test Resource"
2222
private val testResourceDescription = "A test resource for integration testing"
@@ -57,7 +57,7 @@ class ResourceIntegrationTest : KotlinTestBase() {
5757
}
5858

5959
@Test
60-
fun testListResources() = runTest {
60+
fun testListResources() = runBlocking(Dispatchers.IO) {
6161
val result = client.listResources()
6262

6363
assertNotNull(result, "List resources result should not be null")
@@ -70,7 +70,7 @@ class ResourceIntegrationTest : KotlinTestBase() {
7070
}
7171

7272
@Test
73-
fun testReadResource() = runTest {
73+
fun testReadResource() = runBlocking(Dispatchers.IO) {
7474
val result = client.readResource(ReadResourceRequest(uri = testResourceUri))
7575

7676
assertNotNull(result, "Read resource result should not be null")
@@ -83,7 +83,7 @@ class ResourceIntegrationTest : KotlinTestBase() {
8383

8484
@Test
8585
fun testSubscribeAndUnsubscribe() {
86-
runTest {
86+
runBlocking(Dispatchers.IO) {
8787
val subscribeResult = client.subscribeResource(SubscribeRequest(uri = testResourceUri))
8888
assertNotNull(subscribeResult, "Subscribe result should not be null")
8989

0 commit comments

Comments
 (0)