FastMCP 服务端测试实战:用 pytest-asyncio 为 MCP Server 搭建完整测试体系

发布时间:2026/9/11 7:27:59
FastMCP 服务端测试实战:用 pytest-asyncio 为 MCP Server 搭建完整测试体系 FastMCP 服务端测试实战用 pytest-asyncio 为 MCP Server 搭建完整测试体系【免费下载链接】fastmcp The fast, Pythonic way to build MCP servers and clients.项目地址: https://gitcode.com/GitHub_Trending/fa/fastmcp本文以仓库 examples/testing_demo 为例系统讲解如何为 FastMCP Server 编写自动化测试从 pytest-asyncio 环境配置、异步客户端 fixture到对 tools、resources、prompts 三类核心组件的断言方式再到参数化测试与 dirty-equals 模式匹配。读完本文你将掌握一套可复制的 FastMCP 测试模板能直接套用在自己构建的 MCP 服务上。为什么需要为 MCP Server 写测试FastMCP 官方测试文档 docs/servers/testing.mdx 开篇即指出确保 FastMCP Server 可靠且可维护的最佳方式就是测试它。FastMCP Client 与 Pytest 的组合提供了简单而强大的测试手段。其核心思路是测试代码不再通过真实的网络传输去连接远端服务而是把被测的FastMCP服务实例直接注入内存中的Client让测试代码以 MCP 客户端视角调用服务端暴露的工具、资源和提示词。这样既能获得接近真实调用链路的覆盖度又能避免在开发阶段依赖 MCP Inspector 之类的独立调试工具实现紧凑的开发循环。项目结构一览testing_demo 示例的完整结构如下与 README 一致testing_demo/ ├── pyproject.toml # 项目配置含 pytest-asyncio 设置 ├── server.py # 简单的 MCP 服务器tools/resources/prompts ├── tests/ │ └── test_server.py # 完整测试套件 └── README.md # 说明文档对应的仓库路径为 examples/testing_demo/pyproject.toml、examples/testing_demo/server.py 和 examples/testing_demo/tests/test_server.py。第一步pyproject.toml 中的 pytest-asyncio 配置异步是 MCP 交互的默认形态因此测试框架必须能原生处理async def测试函数与 fixture。pyproject.toml中关键的配置只有一行[tool.pytest.ini_options] asyncio_mode autoasyncio_mode auto的作用是pytest 自动把 async 测试函数和 async fixture 纳入 asyncio 事件循环运行无需在每个 async 测试上手动加pytest.mark.asyncio大幅减少样板代码。demo 的 pyproject.toml 还给出了配套的完整配置项[project] name testing-demo version 0.1.0 description FastMCP testing example demonstrating pytest-asyncio patterns readme README.md requires-python 3.10 dependencies [ fastmcp2.0.0, pytest9.0.3, pytest-asyncio1.2.0, dirty-equals0.9.0, ] [tool.pytest.ini_options] asyncio_mode auto testpaths [tests] pythonpath [.] python_files [test_*.py]几个值得注意的配置项testpaths [tests]限定 pytest 只扫描tests目录加快收集速度pythonpath [.]把项目根目录加入sys.path使测试中from server import mcp这类导入能够直接命中根目录下的server.pypython_files [test_*.py]只收集以test_开头的 Python 文件依赖中fastmcp2.0.0是运行被测服务器所需pytest、pytest-asyncio、dirty-equals分别承担测试框架、异步支持和灵活断言。第二步被测试的服务器——tools / resources / prompts 三件套server.py 是一个刻意保持精简、但覆盖三种核心 MCP 组件类型的服务器便于演示各类断言手法。工具Tools三个工具分别演示同步、带默认参数、异步三种形态from fastmcp import FastMCP mcp FastMCP(Testing Demo) mcp.tool def add(a: int, b: int) - int: Add two numbers together return a b mcp.tool def greet(name: str, greeting: str Hello) - str: Greet someone with a customizable greeting return f{greeting}, {name}! mcp.tool async def async_multiply(x: float, y: float) - float: Multiply two numbers (async example) return x * y其中async_multiply展示的是 FastMCP 对异步工具的原生支持——即便服务端是异步实现测试端通过client.call_tool调用时并不需要区别对待。资源Resources一个静态资源加一个带路径参数模板资源mcp.resource(demo://info) def server_info() - str: Get server information return This is the FastMCP Testing Demo server mcp.resource(demo://greeting/{name}) def greeting_resource(name: str) - str: Get a personalized greeting resource return fWelcome to FastMCP, {name}!demo://greeting/{name}中的{name}是 URI 模板变量客户端可直接以demo://greeting/Charlie形式读取。提示词Prompts两个提示词其中explain演示了基于参数的输出分支mcp.prompt(hello) def hello_prompt(name: str World) - str: Generate a hello world prompt return fSay hello to {name} in a friendly way. mcp.prompt(explain) def explain_prompt(topic: str, detail_level: str medium) - str: Generate a prompt to explain a topic if detail_level simple: return fExplain {topic} in simple terms for beginners. elif detail_level detailed: return fProvide a detailed, technical explanation of {topic}. else: return fExplain {topic} with moderate technical detail.第三步异步客户端 fixture——测试的核心基建测试套件的枢纽是一个返回Client的 async fixture见 tests/test_server.pyimport pytest from dirty_equals import IsStr from fastmcp.client import Client pytest.fixture async def client(): Client fixture for testing. Uses async context manager and yields client synchronously. No pytest.mark.asyncio needed - asyncio_mode auto handles it. # Import here to avoid import-time side effects from server import mcp async with Client(mcp) as client: yield client要点拆解from server import mcp放在 fixture 内部避免模块导入期副作用也保证每次测试都拿到该会话下的实例async with Client(mcp)这是 FastMCP 的内存传输模式。Client构造函数的第一个位置参数可以直接接收FastMCP服务实例内部会自动选择FastMCPTransport见 fastmcp_slim/fastmcp/client/transports/init.py 中from fastmcp.client.transports.memory import FastMCPTransport该传输在进程内把客户端与服务端连接起来无需启动任何真实进程或端口yield而非returnpytest fixture 的 teardown 逻辑关闭客户端、清理会话在async with退出时自动执行不需要pytest.mark.asyncio因为asyncio_mode auto已经接管了 async fixture 与 async 测试函数的调度。官方测试文档 docs/servers/testing.mdx 给出的 fixture 写法与此同源差别仅在于显式标注了传输类型pytest.fixture async def main_mcp_client(): async with Client(transportmcp) as mcp_client: yield mcp_client即Client(transport...)与Client(mcp)两种写法等价FastMCP 会为FastMCP实例自动推断内存传输。第四步工具测试——调用与断言.data工具的断言模式统一为result await client.call_tool(name, arguments)然后检查result.dataasync def test_add_tool(client: Client): Test the add tool with simple addition result await client.call_tool(add, {a: 2, b: 3}) assert result.data 5 async def test_greet_tool_default(client: Client): Test the greet tool with default greeting result await client.call_tool(greet, {name: Alice}) assert result.data Hello, Alice! async def test_greet_tool_custom(client: Client): Test the greet tool with custom greeting result await client.call_tool(greet, {name: Bob, greeting: Hi}) assert result.data Hi, Bob! async def test_async_multiply_tool(client: Client): Test the async multiply tool result await client.call_tool(async_multiply, {x: 3.5, y: 2.0}) assert result.data 7.0这里覆盖了三条边界默认参数greet不传greeting时回退到Hello自定义参数显式传入greeting覆盖默认值异步工具async_multiply即使服务端是async def客户端调用方式与同步工具完全一致。从源码看call_tool属于 ClientToolsMixin该 mixin 同时提供list_tools_mcp/list_tools自动分页默认最多 250 页见AUTO_PAGINATION_MAX_PAGES等方法均在 fastmcp_slim/fastmcp/client/client.py 的Client主类中聚合。第五步资源测试——静态与模板资源资源的读取通过client.read_resource(uri)返回一个内容对象列表async def test_server_info_resource(client: Client): Test the server info resource result await client.read_resource(demo://info) assert len(result) 1 assert result[0].text This is the FastMCP Testing Demo server async def test_greeting_resource_template(client: Client): Test the greeting resource template result await client.read_resource(demo://greeting/Charlie) assert len(result) 1 assert result[0].text Welcome to FastMCP, Charlie!两个值得注意的断言细节read_resource总是返回列表这与源码 ClientResourcesMixin.read_resource 的实现一致——它返回list[mcp_types.TextResourceContents | mcp_types.BlobResourceContents]因此先断言len(result) 1再取result[0]模板资源直接以填充后的 URI 访问demo://greeting/Charlie中的Charlie会被服务端模板函数接收为name参数。源码中还支持version、meta等可选参数version会合并进meta[fastmcp][version]请求级元数据普通测试场景下不需要传入。第六步提示词测试——多参数与分支逻辑提示词通过client.get_prompt(name, arguments)获取断言重点落在result.messages[0].content.textasync def test_hello_prompt_default(client: Client): Test hello prompt with default parameter result await client.get_prompt(hello) assert result.messages[0].content.text Say hello to World in a friendly way. async def test_hello_prompt_custom(client: Client): Test hello prompt with custom name result await client.get_prompt(hello, {name: Dave}) assert result.messages[0].content.text Say hello to Dave in a friendly way. async def test_explain_prompt_levels(client: Client): Test explain prompt with different detail levels # Simple level result await client.get_prompt( explain, {topic: MCP, detail_level: simple} ) assert simple terms in result.messages[0].content.text assert MCP in result.messages[0].content.text # Detailed level result await client.get_prompt( explain, {topic: MCP, detail_level: detailed} ) assert detailed in result.messages[0].content.text assert technical in result.messages[0].content.text两个技巧默认参数测试get_prompt(hello)不传参数验证nameWorld默认值生效子串断言代替全等断言explain有simple/detailed/ 默认medium三档输出分支用in判断关键片段既验证了分支逻辑又避免了对整句的强耦合。get_prompt由 ClientPromptsMixin 提供其底层list_prompts_mcp/list_prompts同样支持自动分页负责与服务端完成协议交互。第七步目录服务测试——list_tools / list_resources / list_prompts除了调用单个组件还应验证服务端有哪些东西async def test_list_tools(client: Client): Test listing available tools tools await client.list_tools() tool_names [tool.name for tool in tools] assert add in tool_names assert greet in tool_names assert async_multiply in tool_names async def test_list_resources(client: Client): Test listing available resources resources await client.list_resources() resource_uris [str(resource.uri) for resource in resources] # Check that we have at least the static resource assert demo://info in resource_uris # There should be at least one resource listed assert len(resource_uris) 1 async def test_list_prompts(client: Client): Test listing available prompts prompts await client.list_prompts() prompt_names [prompt.name for prompt in prompts] assert hello in prompt_names assert explain in prompt_names这类测试适合作为契约性回归测试当后续给服务器新增或重命名组件时这里会第一时间暴露问题。注意资源 URI 被str()包装后再断言——这是为了让mcp_types的AnyUrl对象与字符串常量直接比较。第八步参数化测试——一次覆盖多组输入pytest.mark.parametrize让同一断言逻辑覆盖多组输入组合pytest.mark.parametrize( a,b,expected, [ (0, 0, 0), (1, 1, 2), (-1, 1, 0), (100, 200, 300), ], ) async def test_add_parametrized(client: Client, a: int, b: int, expected: int): Test add tool with multiple parameter combinations result await client.call_tool(add, {a: a, b: b}) assert result.data expected这里四组数据覆盖了零值、正数、负数混合与较大数值四类典型输入运行 pytest 时会生成 4 个独立的测试用例任何一个失败都能精确定位到具体输入组合。官方文档 docs/servers/testing.mdx 中test_add的例子也采用了完全相同的模式可作为扩展参考。第九步灵活断言——dirty-equals 与 inline-snapshot用 dirty-equals 做模式匹配当结果包含动态或非确定值如时间戳、随机 ID、按名字变化的内容时精确全等断言会变得脆弱。dirty-equals 提供IsStr(regex...)等模式匹配器from dirty_equals import IsStr # Example using dirty-equals for flexible assertions async def test_greet_with_dirty_equals(client: Client): Test greet tool using dirty-equals for pattern matching result await client.call_tool(greet, {name: Eve}) # Check that result data matches the pattern assert result.data IsStr(regexr^Hello, \w!$)^Hello, \w!$表示以Hello,开头、中间是任意单词字符、以!结尾的字符串这样即便名字部分变化断言依然成立。用 inline-snapshot 固化复杂结构对于来自 MCP Server 的复杂数据结构如工具 schema、完整列表官方文档推荐 inline-snapshot 库依赖清单中未强制包含可按需添加写法形如from inline_snapshot import snapshot async def test_list_tools(main_mcp_client: Client[FastMCPTransport]): list_tools await main_mcp_client.list_tools() assert list_tools snapshot()先用空snapshot()占位再运行pytest --inline-snapshotfix,create把真实数据写回代码中。此后数据一旦变化测试会明确报出差值。结构化断言直接校验工具 Schemademo 还演示了不依赖快照库、直接手工断言 schema 结构的方式# Example using inline-snapshot for complex data async def test_tool_schema_structure(client: Client): Test tool schema structure tools await client.list_tools() add_tool next(tool for tool in tools if tool.name add) # Verify basic schema structure assert add_tool.name add assert add_tool.description Add two numbers together assert a in add_tool.inputSchema[properties] assert b in add_tool.inputSchema[properties] assert add_tool.inputSchema[properties][a][type] integer assert add_tool.inputSchema[properties][b][type] integer这里验证了工具名、描述文本以及inputSchema中每个属性的存在性与 JSON Schema 类型。这类测试能捕获参数名拼写变更类型从 int 改成 float之类的破坏性改动——它们往往不会让工具调用报错却会破坏客户端侧的 schema 依赖。运行测试与服务器运行测试# 安装依赖 uv sync # 运行全部测试 uv run pytest # 带详细输出 uv run pytest -v # 运行单个测试 uv run pytest tests/test_server.py::test_add_tool如果未使用 uv等效命令为pip install -e .或pip install -r等价方式安装依赖后直接运行pytest。运行uv run pytest -v时参数化用例会显示为test_add_parametrized[0-0-0]、test_add_parametrized[1-1-2]等独立条目便于定位失败的具体输入。运行与被检视服务器# 运行服务器 uv run fastmcp run server.py # 检视服务器列出工具、资源、提示词 uv run fastmcp inspect server.pyfastmcp run以 MCP 服务模式启动server.pyfastmcp inspect则输出服务端暴露的组件清单可用于在写测试前快速核对预期名称与参数。小结testing_demo 示例把 FastMCP 服务端测试拆解为一条清晰的生产力路径pyproject 一处配置开启 auto 异步模式 → fixture 内以内存传输包装 Client → 按组件类型选择断言策略。其中内存传输 Client API的组合让测试无需任何外部进程与网络配置即可覆盖 tools / resources / prompts 的完整调用链路。若需进一步系统化你的测试方案可以继续阅读官方 Testing Documentation含 inline-snapshot、dirty-equals、参数化等完整指南并参考仓库 tests 目录下数千个既有测试用例覆盖远程服务器连接、工具/资源/提示词测试等场景获取更多灵感。【免费下载链接】fastmcp The fast, Pythonic way to build MCP servers and clients.项目地址: https://gitcode.com/GitHub_Trending/fa/fastmcp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考