mcp测试nl
通过MCP进行自然语言移动测试
用简单的英语写移动测试。LLM代理(Claude)解释您的指令,并在设备上执行正确的MCP工具调用。
# MCP test — natural language
async def test_valid_credentials(self, mcp):
await mcp.execute(
"Clear the email field, then type 'test@example.com'. "
"Clear the password field, then type 'password123'. "
"Hide the keyboard and tap the login button."
)
await mcp.verify(
"The user should be on the Home screen with a welcome message "
"and a logout button visible."
)与用普通Appium编写的相同测试进行比较:
# Non-MCP test — explicit Appium API calls
def test_valid_credentials(self, driver):
email = driver.find_element(AppiumBy.ID, "com.testlogin:id/email_input")
email.clear()
email.send_keys("test@example.com")
password = driver.find_element(AppiumBy.ID, "com.testlogin:id/password_input")
password.clear()
password.send_keys("password123")
driver.hide_keyboard()
driver.find_element(AppiumBy.ID, "com.testlogin:id/login_button").click()
time.sleep(2)
assert driver.find_element(AppiumBy.ID, "com.testlogin:id/welcome_text").is_displayed()
assert driver.find_element(AppiumBy.ID, "com.testlogin:id/logout_button").is_displayed()______________________________________________________________________
项目结构
mcp-test-nl/
├── README.md
├── .env.example # Environment variable template
├── requirements.txt # Python dependencies
├── pytest.ini # Pytest configuration
├── test_app/ # Android test APK + source
│ └── test-login-app.apk
└── tests/
├── __init__.py
├── conftest.py # Registers the mcp fixture
├── mcp_fixtures.py # MCPAgent (execute + verify) + transport config
├── test_login.py # MCP natural language tests
└── non_mcp/ # Plain Appium tests (for comparison)
├── __init__.py
├── conftest.py # Appium driver fixture
└── test_login_appium.py______________________________________________________________________
MCP代理测试的工作原理
Test: "Login with email test@example.com and password password123"
│
▼
MCPAgent.execute(instruction)
│
├─ Discovers available tools from the MCP server (list_tools)
├─ Sends instruction + tool definitions to Claude API
│ │
│ ▼
│ Claude decides: call clear_text(), type_text(), tap(), etc.
│ │
│ ▼
├─ Executes those tool calls on the device via MCP
├─ Feeds results back to Claude (agentic loop)
└─ Repeats until Claude completes all steps
│
▼
MCPAgent.verify(assertion)
│
├─ Claude calls get_page_source() via MCP
├─ Inspects the screen state
└─ Returns PASS or raises AssertionError with FAIL reason测试代码对底层工具一无所知。它与 任何MCP服务器 --Appium、Slack、GitHub或任何通过MCP公开工具的东西。
______________________________________________________________________
设置
先决条件
- Python 3.10+
- 运行在本地主机4723上的Appium服务器
- 安装了测试应用程序的Android模拟器或设备
- MCP服务器(本地或远程)
安装
cd mcp-test-nl
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt配置
cp .env.example .env
# Edit .env with your values______________________________________________________________________
运行测试
仅MCP测试(不包括非MCP)
ANTHROPIC_API_KEY=sk-ant-... \
MCP_SERVER_DIR=/path/to/appium_mcp_server \
pytest tests/test_login.py -v -s仅限非MPCP测试(普通Appium,不需要API密钥)
pytest tests/non_mcp/ -v -s所有测试
ANTHROPIC_API_KEY=sk-ant-... \
MCP_SERVER_DIR=/path/to/appium_mcp_server \
pytest -v -s______________________________________________________________________
MCP传输:stdio与SSE
MCP夹具支持两种传输模式。这决定了 测试如何连接到MCP服务器.
stdio——本地MCP服务器
夹具将MCP服务器作为 本地子流程 并通过stdin/stdout进行通信。服务器代码必须在您的计算机上。
ANTHROPIC_API_KEY=sk-ant-... \
MCP_TRANSPORT=stdio \
MCP_SERVER_DIR=/path/to/appium_mcp_server \
pytest tests/test_login.py -v -sMCP_TRANSPORT=stdio 是默认值,因此可以省略它:
ANTHROPIC_API_KEY=sk-ant-... \
MCP_SERVER_DIR=/path/to/appium_mcp_server \
pytest tests/test_login.py -v -s空气中的介质:
| 变量 | 默认值 | 描述 |
|---|---|---|
MCP_SERVER_DIR | (必填) | MCP服务器项目的根目录 |
MCP_SERVER_SCRIPT | server.py | 服务器脚本相对于 MCP_SERVER_DIR |
MCP_SERVER_PYTHON | .venv/bin/python | Python二进制相对 MCP_SERVER_DIR |
SSE——远程MCP服务器
夹具连接到 已在运行MCP服务器 通过HTTP使用服务器发送事件。本地不需要服务器代码,只需要URL。
ANTHROPIC_API_KEY=sk-ant-... \
MCP_TRANSPORT=sse \
MCP_SERVER_URL=http://your-server.com/sse \
pytest tests/test_login.py -v -sSSE信封:
| 变量 | 默认值 | 描述 |
|---|---|---|
MCP_SERVER_URL | (必填) | SSE端点的完整URL |
不同场景的SSE示例:
# MCP server on a colleague's machine
MCP_SERVER_URL=http://192.168.1.50:8000/sse
# MCP server deployed on cloud
MCP_SERVER_URL=https://mcp.your-company.com/sse
# MCP server running in Docker locally
MCP_SERVER_URL=http://localhost:8000/sse何时使用哪个
| 站起来 | ||
|---|---|---|
| MCP服务器在您的计算机上 | 是 | - |
| MCP服务器远程托管 | - | 是 |
| MCP服务器在Docker/云中运行 | - | 是 |
| 需要本地服务器源代码 | 是 | 否 |
| 它如何连接 | 启动子进程 | HTTP到正在运行的服务器 |
______________________________________________________________________
MCPAgent API
这 mcp 夹具产生 MCPAgent 有两种方法:
await mcp.execute(instruction: str) -> str
在设备上执行自然语言指令。
- Claude收到指令以及MCP服务器的可用工具
- 它决定通过代理循环调用哪些工具(以及以何种顺序)
- 返回所做工作的文本摘要
await mcp.execute("Type 'hello' into the search bar and tap the search button")await mcp.verify(assertion: str) -> None
验证当前屏幕状态的条件。
- Claude使用MCP工具检查设备屏幕
- 加薪
AssertionError如果条件不满足,则有失败的原因
await mcp.verify("Search results should be displayed with at least one item")______________________________________________________________________
MCP与非MCP:并排比较
测试代码
MCP(自然语言) — tests/test_login.py
async def test_valid_credentials(self, mcp):
await mcp.execute(
"Clear the email field, then type 'test@example.com'. "
"Clear the password field, then type 'password123'. "
"Hide the keyboard and tap the login button."
)
await mcp.verify(
"The user should be on the Home screen with a welcome message "
"and a logout button visible."
)非MCP(普通Appium) — tests/non_mcp/test_login_appium.py
def test_valid_credentials(self, driver):
email = driver.find_element(AppiumBy.ID, "com.testlogin:id/email_input")
email.clear()
email.send_keys("test@example.com")
password = driver.find_element(AppiumBy.ID, "com.testlogin:id/password_input")
password.clear()
password.send_keys("password123")
driver.hide_keyboard()
driver.find_element(AppiumBy.ID, "com.testlogin:id/login_button").click()
time.sleep(2)
assert driver.find_element(AppiumBy.ID, "com.testlogin:id/welcome_text").is_displayed()
assert driver.find_element(AppiumBy.ID, "com.testlogin:id/logout_button").is_displayed()差异
| MCP(自然语言) | 非MCP(普通应用程序) | |
|---|---|---|
| 测试代码 | 简明英文说明 | 元素ID、Appium API调用 |
| 代码行数 | 2次调用(执行+验证) | 每次测试12行以上 |
| 元素定位器 | 测试代码中没有 | 到处都是硬编码 |
| 可读性 | 任何人都能读写 | 需要Appium知识 |
| 当UI更改时 | 说明通常仍然有效 | 必须更新每个损坏的定位器 |
| 谁决定步骤 | 运行时的Claude(LLM) | 编写时的开发人员 |
| 依赖项 | MCP服务器+Anthropic API | 仅限Appium |
| 执行速度 | 较慢(LLM往返) | 较快(直接通话) |
| 成本 | 每次测试运行API使用量 | 免费 |
| 决定论 | LLM可能会改变方法 | 每次都完全相同 |
何时使用哪个
- MCP测试 --当您需要快速的测试编写、可读的测试和对UI更改的弹性时。最适合端到端流,其中“什么”比“如何”更重要。
- 非MCP测试 -当您需要速度、确定性、零API成本或对精确交互的精确控制时。最适合在CI中频繁运行的回归套件。
______________________________________________________________________
编写新测试
MCP测试
在中创建文件 tests/例如。 tests/test_signup.py:
import pytest
@pytest.mark.asyncio
class TestSignup:
async def test_successful_signup(self, mcp):
await mcp.execute(
"Tap the 'Sign Up' link on the login screen. "
"Fill in 'John' for first name, 'Doe' for last name, "
"'john@example.com' for email, and 'SecurePass1!' for password. "
"Tap the Sign Up button."
)
await mcp.verify("A success message or welcome screen should be displayed.")非MCP测试
在中创建文件 tests/non_mcp/:
from appium.webdriver.common.appiumby import AppiumBy
class TestSignup:
def test_successful_signup(self, driver):
driver.find_element(AppiumBy.ID, "com.testlogin:id/signup_link").click()
driver.find_element(AppiumBy.ID, "com.testlogin:id/first_name").send_keys("John")
driver.find_element(AppiumBy.ID, "com.testlogin:id/last_name").send_keys("Doe")
driver.find_element(AppiumBy.ID, "com.testlogin:id/email").send_keys("john@example.com")
driver.find_element(AppiumBy.ID, "com.testlogin:id/password").send_keys("SecurePass1!")
driver.find_element(AppiumBy.ID, "com.testlogin:id/signup_button").click()
assert driver.find_element(AppiumBy.ID, "com.testlogin:id/success_msg").is_displayed()______________________________________________________________________
配置参考
| 变量 | 默认值 | 描述 |
|---|---|---|
ANTHROPIC_API_KEY | (MCP测试需要) | Claude的Anthropic API密钥 |
MCP_TRANSPORT | stdio | 运输类型: stdio 或 sse |
MCP_SERVER_DIR | (stdio需要) | 本地MCP服务器的根目录 |
MCP_SERVER_SCRIPT | server.py | 服务器脚本相对于 MCP_SERVER_DIR |
MCP_SERVER_PYTHON | .venv/bin/python | Python二进制相对 MCP_SERVER_DIR |
MCP_SERVER_URL | (sse必需) | 远程服务器的sse端点URL |
MCP_AGENT_MODEL | claude-sonnet-4-5-20250929 | 代理使用的Claude模型 |
MCP_AGENT_MAX_TURNS | 15 | 每次执行/验证调用的最大工具调用轮次 |
______________________________________________________________________
