测试指南
May 7, 2026 · View on GitHub
本指南详细说明 Godot MCP Native 项目的测试策略、测试工具和最佳实践。
目录
测试概述
测试金字塔
__
/ \
/ E2E \ 端到端测试(少量,关键路径)
/______\
/ \
/ Integration\ 集成测试(中量,API 级别)
/__________\
/ \
/ Unit Tests \ 单元测试(大量,函数级别)
/________________\
测试类型
| 测试类型 | 用途 | 工具 | 覆盖率目标 |
|---|---|---|---|
| 单元测试 | 测试单个函数/类 | Vitest, Godot | 80% |
| 集成测试 | 测试模块交互 | Python, curl | 关键路径 100% |
| 性能测试 | 测试系统性能 | GDScript | N/A |
| 端到端测试 | 测试完整流程 | Node.js | 核心功能 100% |
测试目录结构
test/
├── unit/ # 单元测试
│ ├── tools/ # 工具单元测试
│ │ ├── create_node.test.ts
│ │ └── delete_node.test.ts
│ └── utils/ # 工具类单元测试
│ └── godot_connection.test.ts
├── integration/ # 集成测试
│ ├── stdio/
│ │ └── test_mcp_stdio.py
│ └── http/
│ └── test_mcp_http.py
├── performance/ # 性能测试
│ └── performance_test.gd
├── e2e/ # 端到端测试
│ ├── test_mcp_client.js
│ └── test_mcp_client.ts
└── helpers/ # 测试辅助工具
├── mcp_client.js
└── godot_mock.gd
单元测试
GDScript 单元测试
工具:GUT (Godot Unit Test) 9.6.0
配置文件:.gutconfig.json(项目根目录)
{
"dirs": ["res://test/unit/"],
"include_subdirs": true,
"log_level": 2,
"should_maximize": false,
"should_exit_on_finish": false,
"ignore_pause": true,
"suffix": ".gd",
"panel_options": {
"font_size": 14
}
}
测试目录结构:
test/unit/
├── test_http_parsing.gd
├── test_mcp_auth_manager.gd
├── test_mcp_http_server.gd
├── test_mcp_resource_manager.gd
├── test_mcp_server_core.gd
├── test_mcp_server_native.gd
├── test_mcp_stdio_server.gd
├── test_mcp_transport_base.gd
├── test_mcp_types.gd
├── test_node_tools_convert.gd
├── test_path_validator.gd
└── tools/
├── test_debug_tools.gd
├── test_editor_tools.gd
├── test_node_tools.gd
├── test_node_tools_enhanced.gd
├── test_project_tools.gd
├── test_resource_tools.gd
├── test_scene_tools.gd
└── test_script_tools.gd
当前测试规模:19 个测试脚本,295 个测试用例,521 个断言
运行测试:
# 命令行运行 GUT 测试
& "f:/Godot/Godot_v4.6.1-stable_win64.exe" --headless --path "F:/gitProjects/Godot-MCP-Native" -s addons/gut/gut_cmdln.gd -gdir=res://test/unit/ -ginclude_subdirs -gexit
示例测试 (test/unit/tools/test_node_tools_enhanced.gd):
extends "res://addons/gut/test.gd"
var _node_tools: RefCounted = null
func before_each():
_node_tools = load("res://addons/godot_mcp/tools/node_tools_native.gd").new()
func after_each():
_node_tools = null
func test_duplicate_node_missing_node_path():
var result: Dictionary = _node_tools._tool_duplicate_node({})
assert_has(result, "error", "Should return error for missing node_path")
func test_rename_node_basic():
var node: Node = Node.new()
node.name = "OldName"
add_child_autofree(node)
node.name = "NewName"
assert_eq(str(node.name), "NewName", "Node name should be updated")
func test_connect_signal_basic():
var emitter: Button = Button.new()
emitter.name = "Button"
add_child_autofree(emitter)
var signal_list: Array = emitter.get_signal_list()
var has_pressed: bool = false
for sig in signal_list:
if sig.get("name", "") == "pressed":
has_pressed = true
break
assert_true(has_pressed, "Button should have 'pressed' signal")
GUT 断言方法:
| 方法 | 用途 |
|---|---|
assert_true(condition) | 断言条件为真 |
assert_false(condition) | 断言条件为假 |
assert_eq(a, b) | 断言相等 |
assert_ne(a, b) | 断言不等 |
assert_gt(a, b) | 断言大于 |
assert_lt(a, b) | 断言小于 |
assert_has(dict, key) | 断言字典包含键 |
assert_contains(str, substr) | 断言字符串包含子串 |
GUT 注意事项:
- 使用
add_child_autofree(node)将节点添加到场景树并自动释放 - 不要手动调用
node.free(),add_child_autofree会自动处理 - 使用
load("res://path/to/script.gd").new()而非ClassName.new()(CLI 模式下 class_name 不可用) assert_has仅适用于 Dictionary 和 Array,不适用于 String
集成测试
HTTP 模式集成测试
工具:Python + requests
示例测试 (test/http/test_mcp_http.py):
#!/usr/bin/env python3
"""测试 MCP 服务器的 HTTP 模式"""
import requests
import json
BASE_URL = "http://localhost:9080/mcp"
AUTH_TOKEN = "test-token-1234567890"
def test_tools_list():
"""测试 tools/list 端点"""
response = requests.post(
BASE_URL,
json={
"jsonrpc": "2.0",
"method": "tools/list",
"id": 1
},
headers={
"Authorization": f"Bearer {AUTH_TOKEN}"
}
)
assert response.status_code == 200, f"Unexpected status code: {response.status_code}"
data = response.json()
assert "result" in data, "Missing result in response"
assert "tools" in data["result"], "Missing tools in result"
assert len(data["result"]["tools"]) >= 42, "Not enough tools"
print("✓ test_tools_list passed")
def test_unauthorized():
"""测试未授权请求"""
response = requests.post(
BASE_URL,
json={
"jsonrpc": "2.0",
"method": "tools/list",
"id": 1
}
# 不包含 Authorization 头
)
assert response.status_code == 401 or "error" in response.json(), "Should return 401 or error"
print("✓ test_unauthorized passed")
def test_sse_endpoint():
"""测试 SSE 端点"""
import sseclient
response = requests.get(
BASE_URL,
headers={
"Accept": "text/event-stream",
"Authorization": f"Bearer {AUTH_TOKEN}"
},
stream=True
)
assert response.status_code == 200, "SSE connection failed"
client = sseclient.SSEClient(response)
for event in client.events():
assert event.event == "connected", "Unexpected event"
assert "session_id" in event.data, "Missing session_id"
break # 仅测试第一个事件
print("✓ test_sse_endpoint passed")
if __name__ == "__main__":
# 确保服务器正在运行
test_tools_list()
test_unauthorized()
test_sse_endpoint()
print("\n✓ All HTTP integration tests passed")
运行测试:
# 首先启动 HTTP 服务器
cd F:\gitProjects\Godot-MCP
"Godot.exe" --path . --mcp-server --http
# 然后运行测试
cd test/http
python test_mcp_http.py
性能测试
Godot 性能测试
示例 (test/benchmark/performance_test.gd):
extends SceneTree
# 性能测试配置
const TEST_ITERATIONS: int = 100
const MAX_AVERAGE_TIME_MS: float = 10.0
func _ready() -> void:
print("=== Godot-MCP Performance Tests ===")
# 测试 1: create_node 性能
_test_create_node_performance()
# 测试 2: list_nodes 性能
_test_list_nodes_performance()
# 测试 3: get_node_properties 性能
_test_get_node_properties_performance()
print("\n✓ All performance tests passed")
quit()
func _test_create_node_performance() -> void:
print("\nTest: create_node performance")
print(" Iterations: " + str(TEST_ITERATIONS))
var tool_instance: NodeToolsNative = NodeToolsNative.new()
tool_instance.initialize(get_editor_interface())
var total_time: int = 0
for i in range(TEST_ITERATIONS):
var start_time: int = Time.get_ticks_msec()
var result: Dictionary = tool_instance.create_node({
"parent_path": "/root",
"node_type": "Node2D",
"node_name": "PerfTest" + str(i)
})
var elapsed: int = Time.get_ticks_msec() - start_time
total_time += elapsed
assert(result["status"] == "success", "Node creation failed")
var avg_time: float = total_time / float(TEST_ITERATIONS)
var min_time: int = 0 # TODO: 追踪最小值
var max_time: int = 0 # TODO: 追踪最大值
print(" Average time: " + str(avg_time) + "ms")
print(" Total time: " + str(total_time) + "ms")
assert(avg_time < MAX_AVERAGE_TIME_MS, "Performance degradation detected")
print(" ✓ create_node performance")
func _test_list_nodes_performance() -> void:
print("\nTest: list_nodes performance")
# ... 类似实现
func _test_get_node_properties_performance() -> void:
print("\nTest: get_node_properties performance")
# ... 类似实现
运行性能测试:
"Godot.exe" --path "F:\gitProjects\Godot-MCP" --script "test/benchmark/performance_test.gd"
负载测试
工具:Python + threading
示例 (test/load/test_concurrent_requests.py):
#!/usr/bin/env python3
"""并发负载测试"""
import threading
import requests
import time
BASE_URL = "http://localhost:9080/mcp"
AUTH_TOKEN = "test-token-1234567890"
NUM_THREADS = 10
NUM_REQUESTS_PER_THREAD = 100
def worker(thread_id: int):
"""工作线程函数"""
results = {"success": 0, "fail": 0}
for i in range(NUM_REQUESTS_PER_THREAD):
try:
response = requests.post(
BASE_URL,
json={
"jsonrpc": "2.0",
"method": "tools/list",
"id": thread_id * 1000 + i
},
headers={
"Authorization": f"Bearer {AUTH_TOKEN}"
},
timeout=5
)
if response.status_code == 200:
results["success"] += 1
else:
results["fail"] += 1
except Exception as e:
results["fail"] += 1
return results
def test_concurrent_requests():
"""测试并发请求"""
threads = []
results = []
start_time = time.time()
# 创建并启动线程
for i in range(NUM_THREADS):
thread = threading.Thread(target=worker, args=(i,))
threads.append(thread)
thread.start()
# 等待所有线程完成
for thread in threads:
thread.join()
elapsed = time.time() - start_time
# 汇总结果
total_success = 0
total_fail = 0
for result in results:
total_success += result["success"]
total_fail += result["fail"]
total_requests = total_success + total_fail
requests_per_second = total_requests / elapsed
print(f"=== Concurrent Requests Test ===")
print(f"Threads: {NUM_THREADS}")
print(f"Requests per thread: {NUM_REQUESTS_PER_THREAD}")
print(f"Total requests: {total_requests}")
print(f"Success: {total_success}")
print(f"Fail: {total_fail}")
print(f"Elapsed time: {elapsed:.2f}s")
print(f"Requests/second: {requests_per_second:.2f}")
assert total_fail == 0, "Some requests failed"
print("✓ Concurrent requests test passed")
if __name__ == "__main__":
test_concurrent_requests()
测试覆盖率
GDScript 覆盖率
Godot 没有内置的覆盖率工具,但可以使用以下方法:
手动追踪:
var _coverage: Dictionary = {}
func _track_coverage(function_name: String) -> void:
if not _coverage.has(function_name):
_coverage[function_name] = 0
_coverage[function_name] += 1
func _get_coverage_report() -> String:
var report: String = "Coverage Report:\n"
for func_name in _coverage:
report += " " + func_name + ": " + str(_coverage[func_name]) + " calls\n"
return report
使用第三方工具:
- Gut:Godot 单元测试框架,包含覆盖率报告
持续集成
GitHub Actions 配置
示例 (.github/workflows/test.yml):
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
godot-version: ["4.2", "4.3"]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- name: Setup Godot ${{ matrix.godot-version }}
uses: azure/godot-builds@v1
with:
version: ${{ matrix.godot-version }}
- name: Install dependencies
run: |
cd server
npm install
npm run build
- name: Run unit tests
run: |
cd server
npm test
- name: Run integration tests (stdio)
run: |
cd test/stdio
python test_mcp_stdio.py
- name: Run integration tests (http)
run: |
# 启动 Godot 服务器(后台)
godot --path . --mcp-server --http &
sleep 5
# 运行测试
cd test/http
python test_mcp_http.py
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./server/coverage/coverage-final.json
测试最佳实践
1. 测试命名
好的做法:
describe("createNode", () => {
it("should create a new node when valid parameters are provided", () => {
// ...
});
it("should return error when node type is invalid", () => {
// ...
});
});
避免:
describe("createNode", () => {
it("test1", () => {
// ...
});
it("test2", () => {
// ...
});
});
2. 测试独立性
好的做法:
beforeEach(() => {
// 每个测试前重置状态
mockGodot.reset();
});
it("should create node", () => {
// 不依赖其他测试
});
避免:
it("should create node", () => {
// 依赖前一个测试创建的节点
});
3. Mock 外部依赖
好的做法:
// 使用 Mock
const mockGodot = {
createNode: vi.fn().mockReturnValue({ status: "success" }),
};
const result = await createNode(mockGodot, "/root", "Node2D", "Player");
expect(mockGodot.createNode).toHaveBeenCalled();
避免:
// 依赖真实的 Godot 服务器
const realGodot = new GodotConnection();
const result = await createNode(realGodot, "/root", "Node2D", "Player");
4. 测试边界条件
示例:
it("should handle empty string", () => {
const result = await createNode(mockGodot, "", "Node2D", "Player");
expect(result.status).toBe("error");
});
it("should handle null", () => {
const result = await createNode(mockGodot, null, "Node2D", "Player");
expect(result.status).toBe("error");
});
it("should handle very long node name", () => {
const longName = "A".repeat(1000);
const result = await createNode(mockGodot, "/root", "Node2D", longName);
expect(result.status).toBe("error");
});
5. 测试错误信息
示例:
it("should return descriptive error message", async () => {
const result = await createNode(mockGodot, "/root", "InvalidType", "Test");
expect(result.status).toBe("error");
expect(result.message).toContain("Invalid node type");
expect(result.message).toContain("InvalidType");
});
总结
测试是确保代码质量的关键环节。Godot MCP Native 项目采用多层次的测试策略,包括单元测试、集成测试、性能测试和端到端测试。遵循本指南中的最佳实践,可以编写出高质量、可维护的测试代码。
如有任何问题或建议,欢迎在 GitHub Issues 中提出。
Happy Testing! 🧪