Skip to content

Commit c8ad9d2

Browse files
committed
[fel] add unit test for MCP server
1 parent 0b6ee27 commit c8ad9d2

File tree

7 files changed

+269
-4
lines changed

7 files changed

+269
-4
lines changed

framework/fel/java/plugins/tool-mcp-server/pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,20 @@
3232
</dependency>
3333

3434
<!-- Test -->
35+
<dependency>
36+
<groupId>org.junit.jupiter</groupId>
37+
<artifactId>junit-jupiter</artifactId>
38+
<scope>test</scope>
39+
</dependency>
40+
<dependency>
41+
<groupId>org.mockito</groupId>
42+
<artifactId>mockito-core</artifactId>
43+
<scope>test</scope>
44+
</dependency>
3545
<dependency>
3646
<groupId>org.assertj</groupId>
3747
<artifactId>assertj-core</artifactId>
48+
<scope>test</scope>
3849
</dependency>
3950
</dependencies>
4051

framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/InitializeHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class InitializeHandler extends AbstractMessageHandler<InitializeHandler.
2626
* Constructs a new instance of the InitializeHandler class.
2727
*
2828
* @param mcpServer The MCP server instance used to retrieve server information during request handling.
29-
* @throws IllegalStateException If {@code mcpServer} is null.
29+
* @throws IllegalArgumentException If {@code mcpServer} is null.
3030
*/
3131
public InitializeHandler(McpServer mcpServer) {
3232
super(InitializeRequest.class);

framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/ToolCallHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public class ToolCallHandler extends AbstractMessageHandler<ToolCallHandler.Tool
3838
*
3939
* @param mcpServer The MCP server instance used to invoke tools during request handling.
4040
* @param jsonSerializer The serializer used to convert non-string results into JSON strings.
41-
* @throws IllegalStateException If {@code mcpServer} or {@code jsonSerializer} is null.
41+
* @throws IllegalArgumentException If {@code mcpServer} or {@code jsonSerializer} is null.
4242
*/
4343
public ToolCallHandler(McpServer mcpServer, ObjectSerializer jsonSerializer) {
4444
super(ToolCallRequest.class);

framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/ToolListHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class ToolListHandler extends AbstractMessageHandler<ToolListHandler.Tool
2828
* Constructs a new instance of the ToolListHandler class.
2929
*
3030
* @param mcpServer The MCP server instance used to retrieve the list of tools during request handling.
31-
* @throws IllegalStateException If {@code mcpServer} is null.
31+
* @throws IllegalArgumentException If {@code mcpServer} is null.
3232
*/
3333
public ToolListHandler(McpServer mcpServer) {
3434
super(ToolListRequest.class);

framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/support/DefaultMcpServer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public class DefaultMcpServer implements McpServer, ToolChangedObserver {
4141
* Constructs a new instance of the DefaultMcpServer class.
4242
*
4343
* @param toolExecuteService The service used to execute tools when handling tool call requests.
44-
* @throws IllegalStateException If {@code toolExecuteService} is null.
44+
* @throws IllegalArgumentException If {@code toolExecuteService} is null.
4545
*/
4646
public DefaultMcpServer(ToolExecuteService toolExecuteService) {
4747
this.toolExecuteService = notNull(toolExecuteService, "The tool execute service cannot be null.");
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fel.tool.mcp.server;
8+
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
import static org.assertj.core.api.Assertions.catchThrowableOfType;
11+
import static org.mockito.Mockito.mock;
12+
13+
import modelengine.fitframework.serialization.ObjectSerializer;
14+
15+
import org.junit.jupiter.api.BeforeEach;
16+
import org.junit.jupiter.api.DisplayName;
17+
import org.junit.jupiter.api.Nested;
18+
import org.junit.jupiter.api.Test;
19+
20+
/**
21+
* Unit test for {@link McpController}.
22+
*
23+
* @author 季聿阶
24+
* @since 2025-05-20
25+
*/
26+
@DisplayName("Unit tests for McpController")
27+
public class McpControllerTest {
28+
private ObjectSerializer objectSerializer;
29+
private McpServer mcpServer;
30+
private String baseUrl;
31+
32+
@BeforeEach
33+
void setup() {
34+
this.objectSerializer = mock(ObjectSerializer.class);
35+
this.mcpServer = mock(McpServer.class);
36+
this.baseUrl = "http://localhost:8080";
37+
}
38+
39+
@Nested
40+
@DisplayName("Constructor Tests")
41+
class GivenConstructor {
42+
@Test
43+
@DisplayName("Should throw exception when base URL is null or blank")
44+
void shouldThrowExceptionWhenBaseUrlIsNullOrEmpty() {
45+
// Null
46+
var exception1 = catchThrowableOfType(IllegalArgumentException.class,
47+
() -> new McpController(null, objectSerializer, mcpServer));
48+
assertThat(exception1).hasMessage("The base URL for MCP server cannot be blank.");
49+
50+
// Blank
51+
var exception2 = catchThrowableOfType(IllegalArgumentException.class,
52+
() -> new McpController("", objectSerializer, mcpServer));
53+
assertThat(exception2).hasMessage("The base URL for MCP server cannot be blank.");
54+
}
55+
56+
@Test
57+
@DisplayName("Should throw exception when serializer is null")
58+
void shouldThrowExceptionWhenSerializerIsNull() {
59+
var exception = catchThrowableOfType(IllegalArgumentException.class,
60+
() -> new McpController(baseUrl, null, mcpServer));
61+
assertThat(exception).hasMessage("The json serializer cannot be null.");
62+
}
63+
64+
@Test
65+
@DisplayName("Should throw exception when mcpServer is null")
66+
void shouldThrowExceptionWhenMcpServerIsNull() {
67+
var exception = catchThrowableOfType(IllegalArgumentException.class,
68+
() -> new McpController(baseUrl, objectSerializer, null));
69+
assertThat(exception).hasMessage("The MCP server cannot be null.");
70+
}
71+
}
72+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fel.tool.mcp.server.support;
8+
9+
import static modelengine.fitframework.util.ObjectUtils.cast;
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
import static org.assertj.core.api.Assertions.catchThrowableOfType;
12+
import static org.mockito.Mockito.anyMap;
13+
import static org.mockito.Mockito.anyString;
14+
import static org.mockito.Mockito.eq;
15+
import static org.mockito.Mockito.mock;
16+
import static org.mockito.Mockito.times;
17+
import static org.mockito.Mockito.verify;
18+
import static org.mockito.Mockito.when;
19+
20+
import modelengine.fel.tool.mcp.server.McpServer;
21+
import modelengine.fel.tool.mcp.server.entity.ToolEntity;
22+
import modelengine.fel.tool.service.ToolExecuteService;
23+
import modelengine.fitframework.util.MapBuilder;
24+
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.DisplayName;
27+
import org.junit.jupiter.api.Nested;
28+
import org.junit.jupiter.api.Test;
29+
30+
import java.util.List;
31+
import java.util.Map;
32+
33+
/**
34+
* Unit test for {@link DefaultMcpServer}.
35+
*
36+
* @author 季聿阶
37+
* @since 2025-05-20
38+
*/
39+
@DisplayName("Unit tests for DefaultMcpServer")
40+
public class DefaultMcpServerTest {
41+
private ToolExecuteService toolExecuteService;
42+
43+
@BeforeEach
44+
void setup() {
45+
this.toolExecuteService = mock(ToolExecuteService.class);
46+
}
47+
48+
@Nested
49+
@DisplayName("Constructor Tests")
50+
class GivenConstructor {
51+
@Test
52+
@DisplayName("Should throw IllegalArgumentException when toolExecuteService is null")
53+
void throwIllegalArgumentExceptionWhenToolExecuteServiceIsNull() {
54+
IllegalArgumentException exception =
55+
catchThrowableOfType(IllegalArgumentException.class, () -> new DefaultMcpServer(null));
56+
assertThat(exception).isNotNull().hasMessage("The tool execute service cannot be null.");
57+
}
58+
}
59+
60+
@Nested
61+
@DisplayName("getInfo Method Tests")
62+
class GivenGetInfo {
63+
@Test
64+
@DisplayName("Should return expected server information")
65+
void returnExpectedServerInfo() {
66+
McpServer server = new DefaultMcpServer(toolExecuteService);
67+
Map<String, Object> info = server.getInfo();
68+
69+
assertThat(info).containsKey("protocolVersion").containsValue("2025-03-26");
70+
71+
Map<String, Object> capabilities = cast(info.get("capabilities"));
72+
assertThat(capabilities).containsKey("logging").containsKey("tools");
73+
74+
Map<String, Object> toolsCapability = cast(capabilities.get("tools"));
75+
assertThat(toolsCapability).containsEntry("listChanged", true);
76+
77+
Map<String, Object> serverInfo = cast(info.get("serverInfo"));
78+
assertThat(serverInfo).containsEntry("name", "FIT Store MCP Server")
79+
.containsEntry("version", "3.5.0-SNAPSHOT");
80+
}
81+
}
82+
83+
@Nested
84+
@DisplayName("registerToolsChangedObserver and Notification Tests")
85+
class GivenRegisterAndNotify {
86+
@Test
87+
@DisplayName("Should notify observers when tools are added or removed")
88+
void notifyObserversOnToolAddOrRemove() {
89+
DefaultMcpServer server = new DefaultMcpServer(toolExecuteService);
90+
McpServer.ToolsChangedObserver observer = mock(McpServer.ToolsChangedObserver.class);
91+
server.registerToolsChangedObserver(observer);
92+
93+
server.onToolAdded("tool1",
94+
"description1",
95+
MapBuilder.<String, Object>get().put("schema", "value1").build());
96+
verify(observer, times(1)).onToolsChanged();
97+
98+
server.onToolRemoved("tool1");
99+
verify(observer, times(2)).onToolsChanged();
100+
}
101+
}
102+
103+
@Nested
104+
@DisplayName("onToolAdded Method Tests")
105+
class GivenOnToolAdded {
106+
@Test
107+
@DisplayName("Should add tool successfully with valid parameters")
108+
void addToolSuccessfully() {
109+
DefaultMcpServer server = new DefaultMcpServer(toolExecuteService);
110+
String name = "tool1";
111+
String description = "description1";
112+
Map<String, Object> schema = MapBuilder.<String, Object>get().put("input", "value").build();
113+
114+
server.onToolAdded(name, description, schema);
115+
116+
List<ToolEntity> tools = server.getTools();
117+
assertThat(tools).hasSize(1);
118+
119+
ToolEntity tool = tools.get(0);
120+
assertThat(tool.getName()).isEqualTo(name);
121+
assertThat(tool.getDescription()).isEqualTo(description);
122+
assertThat(tool.getInputSchema()).isEqualTo(schema);
123+
}
124+
125+
@Test
126+
@DisplayName("Should ignore invalid parameters and not add any tool")
127+
void ignoreInvalidParameters() {
128+
DefaultMcpServer server = new DefaultMcpServer(toolExecuteService);
129+
130+
server.onToolAdded("", "description", MapBuilder.<String, Object>get().put("input", "value").build());
131+
assertThat(server.getTools()).isEmpty();
132+
133+
server.onToolAdded("tool1", "", MapBuilder.<String, Object>get().put("input", "value").build());
134+
assertThat(server.getTools()).isEmpty();
135+
136+
server.onToolAdded("tool1", "description", null);
137+
assertThat(server.getTools()).isEmpty();
138+
}
139+
}
140+
141+
@Nested
142+
@DisplayName("onToolRemoved Method Tests")
143+
class GivenOnToolRemoved {
144+
@Test
145+
@DisplayName("Should remove an added tool correctly")
146+
void removeToolSuccessfully() {
147+
DefaultMcpServer server = new DefaultMcpServer(toolExecuteService);
148+
server.onToolAdded("tool1", "desc", MapBuilder.<String, Object>get().put("input", "value").build());
149+
150+
server.onToolRemoved("tool1");
151+
152+
assertThat(server.getTools()).isEmpty();
153+
}
154+
155+
@Test
156+
@DisplayName("Should ignore removal if name is blank")
157+
void ignoreBlankName() {
158+
DefaultMcpServer server = new DefaultMcpServer(toolExecuteService);
159+
server.onToolAdded("tool1", "desc", MapBuilder.<String, Object>get().put("input", "value").build());
160+
161+
server.onToolRemoved("");
162+
163+
assertThat(server.getTools()).hasSize(1);
164+
}
165+
}
166+
167+
@Nested
168+
@DisplayName("callTool Method Tests")
169+
class GivenCallTool {
170+
@Test
171+
@DisplayName("Should call the tool and return correct result")
172+
void callToolSuccessfully() {
173+
when(toolExecuteService.execute(anyString(), anyMap())).thenReturn("result");
174+
McpServer server = new DefaultMcpServer(toolExecuteService);
175+
176+
Object result = server.callTool("tool1", Map.of("arg1", "value1"));
177+
178+
assertThat(result).isEqualTo("result");
179+
verify(toolExecuteService, times(1)).execute(eq("tool1"), anyMap());
180+
}
181+
}
182+
}

0 commit comments

Comments
 (0)