Skip to content

Conversation

@kpavlov
Copy link
Contributor

@kpavlov kpavlov commented Oct 23, 2025

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?

  • Unit tests

Breaking Changes

No

Types of changes

  • 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
  • Test improvement

Checklist

  • I have read the MCP Documentation
  • 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

#289 follow-up cc: @maeryo

@kpavlov kpavlov added enhancement New feature or request tests labels Oct 23, 2025
@kpavlov kpavlov force-pushed the kpavlov/refactor-mock-transport branch from 780cf46 to 6abbae5 Compare October 23, 2025 15:49
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.
@kpavlov kpavlov force-pushed the kpavlov/refactor-mock-transport branch from 6abbae5 to 954e17a Compare October 23, 2025 16:00
@kpavlov kpavlov marked this pull request as ready for review October 23, 2025 16:00
@kpavlov kpavlov requested a review from devcrocod October 23, 2025 16:28
Copy link
Contributor

@maeryo maeryo left a 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()
        }
    }
    // ...
}

Comment on lines +69 to +71
val response = requestHandlers.firstOrNull {
it.first.invoke(message)
}?.second?.invoke(message)
Copy link
Contributor

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.

Comment on lines +185 to +194
* @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,
Copy link
Contributor

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".

@kpavlov kpavlov requested review from Ololoshechkin and e5l October 23, 2025 16:47
Comment on lines +243 to +245
// Should work with custom pool interval
val message = transport.awaitMessage(
poolInterval = 10.milliseconds,
Copy link
Contributor

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

@maeryo
Copy link
Contributor

maeryo commented Oct 23, 2025

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 transport instance. Since MockTransport accumulates messages internally, this could cause test interference. Consider either:

  • Adding a clearMessages() method and calling it in @BeforeTest, or
  • Creating fresh transport instances per test when isolation is needed
// 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 assertNotNull(x) + assertTrue(x is Type) is redundant in Kotlin. The type check already ensures non-null:

// Current
assertNotNull(message)
assertTrue(message is JSONRPCResponse)

// Suggested
assertTrue(message is JSONRPCResponse)  // Smart cast handles both checks

These are minor improvements but would help keep the test suite cleaner and more maintainable. WDYT?

Copy link
Contributor

@devcrocod devcrocod left a 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

Comment on lines +3367 to +3386
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;
}
Copy link
Contributor

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

Copy link
Contributor Author

@kpavlov kpavlov Oct 23, 2025

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

@kpavlov kpavlov requested a review from skarpovdev October 23, 2025 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants