-
Notifications
You must be signed in to change notification settings - Fork 169
Refactor MockTransport #333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
780cf46 to
6abbae5
Compare
This replaces bespoke test transport with a reusable, configurable MockTransport in a shared testing package. The new MockTransport supports registering handlers for specific JSON-RPC methods (success and error), records sent/received messages, and provides an awaitMessage helper with polling and timeouts. Tests were added to validate handler registration, auto-responses, error responses, concurrency, and message awaiting behavior. Existing client tests were updated to configure MockTransport via lambdas instead of hardcoded logic. Additionally, coroutine test utilities were added to the core test dependencies.
6abbae5 to
954e17a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The _sentMessages and _receivedMessages lists grow unbounded, which can cause memory issues in long-running or high-volume tests.
Suggestion
private val maxMessages: Int = 1000 // configurable
private val _sentMessages = ArrayDeque<JSONRPCMessage>()
private val _receivedMessages = ArrayDeque<JSONRPCMessage>()
override suspend fun send(message: JSONRPCMessage) {
mutex.withLock {
_sentMessages.add(message)
if (_sentMessages.size > maxMessages) {
_sentMessages.removeFirst()
}
}
// ...
}| val response = requestHandlers.firstOrNull { | ||
| it.first.invoke(message) | ||
| }?.second?.invoke(message) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While ConcurrentSet is used for requestHandlers, the iteration in send() method is not protected by the mutex, which could lead to race conditions when handlers are added/removed concurrently.
| * @param poolInterval The interval at which the function polls the received messages. Default is 50 milliseconds. | ||
| * @param timeout The maximum time to wait for a matching message. Default is 3 seconds. | ||
| * @param timeoutMessage The error message to throw when the timeout is reached. | ||
| * Default is "No message received matching predicate". | ||
| * @param predicate A predicate function that returns true if the message matches the criteria. | ||
| * @return The first JSON-RPC message that matches the predicate. | ||
| */ | ||
| @OptIn(ExperimentalTime::class) | ||
| public suspend fun awaitMessage( | ||
| poolInterval: Duration = 50.milliseconds, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter name poolInterval appears to be a typo. The correct terminology is "pollInterval" (as in "polling"), not "pool".
| // Should work with custom pool interval | ||
| val message = transport.awaitMessage( | ||
| poolInterval = 10.milliseconds, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter name poolInterval appears to be a typo and should be pollInterval (consistent with the polling pattern). This is the same issue found in MockTransport.kt
|
I noticed a few areas that could enhance test maintainability and reliability (MockTransportTest.kt) : 1. Code Duplication (Lines 338-339, 374, 432, 472, 514, 549)The same transport setup pattern is repeated across multiple tests. Consider extracting a helper: private fun createCustomTransport(configure: MockTransport.() -> Unit = {}): MockTransport {
return MockTransport(configure).apply { onMessage { } }
}2. Test Isolation (Lines 29-48)All tests share the same
// Remove shared transport property and create in each test
@Test
fun `some test`() = runTest {
val transport = MockTransport {
onMessageReplyResult(Method.Defined.Initialize) { ... }
}
transport.onMessage { }
// test logic
}3. Redundant Assertions (Throughout)The pattern // Current
assertNotNull(message)
assertTrue(message is JSONRPCResponse)
// Suggested
assertTrue(message is JSONRPCResponse) // Smart cast handles both checksThese are minor improvements but would help keep the test suite cleaner and more maintainable. WDYT? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please consider keeping MockTransport within the test code rather than exposing it in the public API
| public class io/modelcontextprotocol/kotlin/sdk/testing/MockTransport : io/modelcontextprotocol/kotlin/sdk/shared/Transport { | ||
| public fun <init> ()V | ||
| public fun <init> (Lkotlin/jvm/functions/Function1;)V | ||
| public synthetic fun <init> (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V | ||
| public final fun awaitMessage-ePrTys8 (JJLjava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; | ||
| public static synthetic fun awaitMessage-ePrTys8$default (Lio/modelcontextprotocol/kotlin/sdk/testing/MockTransport;JJLjava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; | ||
| public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; | ||
| public final fun getReceivedMessages (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; | ||
| public final fun getSentMessages (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; | ||
| public fun onClose (Lkotlin/jvm/functions/Function0;)V | ||
| public fun onError (Lkotlin/jvm/functions/Function1;)V | ||
| public fun onMessage (Lkotlin/jvm/functions/Function2;)V | ||
| public final fun onMessageReply (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V | ||
| public final fun onMessageReplyError (Lio/modelcontextprotocol/kotlin/sdk/Method;Lkotlin/jvm/functions/Function1;)V | ||
| public static synthetic fun onMessageReplyError$default (Lio/modelcontextprotocol/kotlin/sdk/testing/MockTransport;Lio/modelcontextprotocol/kotlin/sdk/Method;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V | ||
| public final fun onMessageReplyResult (Lio/modelcontextprotocol/kotlin/sdk/Method;Lkotlin/jvm/functions/Function1;)V | ||
| public fun send (Lio/modelcontextprotocol/kotlin/sdk/JSONRPCMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; | ||
| public final fun setupInitializationResponse ()V | ||
| public fun start (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m not sure why MockTransport needs to be added to the core module. It makes the core module more complex, and such things don’t really belong there.
In my opinion, there should be a separate module for mocks that’s used exclusively for testing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@devcrocod feel free to continue and move MockTransport to test classes and finish or drop this PR
Refactor MockTransport
This replaces bespoke test transport with a reusable, configurable MockTransport in a shared testing package.
The new MockTransport supports registering handlers for specific JSON-RPC methods (success and error), records sent/received messages, and provides an awaitMessage helper with polling and timeouts. Tests were added to validate handler registration, auto-responses, error responses, concurrency, and message awaiting behavior. Existing client tests were updated to configure MockTransport via lambdas instead of hardcoded logic. Additionally, coroutine test utilities were added to the core test dependencies.
Motivation and Context
To provide better reusable test utilities
How Has This Been Tested?
Breaking Changes
No
Types of changes
Checklist
Additional context
#289 follow-up cc: @maeryo