当 AI Agent 学会了自己写测试用例:用 MCP + Python 打造自动化测试永动机
当 AI Agent 学会了自己写测试用例:用 MCP + Python 打造自动化测试"永动机"
摘要: 你还在手动写测试用例?AI Agent 已经学会自己打开浏览器、自己点按钮、自己断言结果了。本文带你用 MCP(Model Context Protocol)+ Python 搭建一套"AI 自己测自己"的自动化测试框架,让测试工程师从"人肉点点点"中解放出来。
前言:一个测试工程师的"中年危机"
前几天,隔壁工位的老王跟我吐槽:“我干了八年功能测试,现在 AI 要来抢我饭碗了,我是不是该去送外卖了?”
我说:“别慌,AI 不是来抢你饭碗的——它是来帮你砸键盘的。”
想想看,测试工程师的日常是什么?
- 打开浏览器 → 输入 URL → 点登录 → 填表单 → 点提交 → 检查结果
- 重复以上步骤 200 遍
- 每次版本更新,再来 200 遍
- 眼睛看花了,手指点麻了,灵魂出窍了
这种机械重复的工作,不交给 AI 简直是暴殄天物。
但问题来了:传统的自动化测试(Selenium、Playwright)需要你提前写好脚本,而写脚本本身就是个体力活。如果页面改了,脚本还得跟着改,维护成本比手动测试还高。
那如果……AI 能自己看懂页面、自己决定怎么测试、自己写代码执行呢?
这就是今天要聊的——MCP + AI Agent 自动化测试框架。
一、什么是 MCP?为什么它能改变测试?
MCP(Model Context Protocol)是 Anthropic 提出的一个协议,简单来说就是给 AI 装上了"手脚"。
以前的 AI 只能"说"——你问它问题,它给你文字回答。
有了 MCP,AI 能"做"——它可以:
- 🌐 操控浏览器:打开网页、点击按钮、填写表单
- 📁 读写文件:保存测试报告、读取配置
- 🖥️ 执行命令:运行 Python 脚本、调用 API
- 🗄️ 操作数据库:验证数据是否正确写入
打个比方:
以前的 AI 是个"嘴强王者"——说得头头是道,但啥也不干。
有了 MCP 的 AI 是个"行动派"——不仅给你方案,还帮你把活干了。
MCP 架构图
┌─────────────┐ MCP协议 ┌──────────────┐
│ AI Agent │ ◄─────────────► │ MCP Server │
│ (大脑) │ │ (手脚) │
└─────────────┘ └──────┬───────┘
│
┌──────────┬───────┼───────┬──────────┐
▼ ▼ ▼ ▼ ▼
浏览器 文件系统 Shell 数据库 API
二、环境搭建:五分钟搞定
2.1 安装依赖
# 创建虚拟环境
python -m venv ai-test-env
source ai-test-env/bin/activate # Windows: ai-test-env\Scripts\activate
# 安装核心依赖
pip install playwright anthropic python-dotenv
# 安装浏览器驱动
playwright install chromium
2.2 配置 MCP Server
创建 mcp_config.json:
{
"mcpServers": {
"browser": {
"command": "npx",
"args": ["@anthropic/mcp-server-playwright"],
"env": {
"PLAYWRIGHT_BROWSER": "chromium"
}
},
"filesystem": {
"command": "npx",
"args": ["@anthropic/mcp-server-filesystem", "/tmp/test-reports"]
}
}
}
2.3 获取 API Key
去 Anthropic Console 申请 API Key,设置环境变量:
export ANTHROPIC_API_KEY="sk-ant-xxxxx"
注意: API Key 不要硬编码到代码里!用环境变量或者
.env文件管理,不然你的 Key 会出现在 Git 历史里,然后被爬虫抓走,然后你的钱包就空了。(别问我怎么知道的 😭)
三、核心代码:让 AI 自己测试
3.1 基础版:AI 测试 Agent
"""
AI 自动化测试 Agent
让 AI 自己看页面、自己决定测什么、自己执行
"""
import asyncio
import json
from anthropic import Anthropic
from playwright.async_api import async_playwright
class AITestAgent:
"""AI 驱动的自动化测试 Agent"""
def __init__(self, api_key: str):
self.client = Anthropic(api_key=api_key)
self.browser = None
self.page = None
self.test_results = []
async def init_browser(self, headless: bool = True):
"""初始化浏览器"""
self.playwright = await async_playwright().start()
self.browser = await self.playwright.chromium.launch(
headless=headless
)
self.page = await self.browser.new_page()
print("🚀 浏览器已启动")
async def navigate(self, url: str):
"""导航到指定页面"""
await self.page.goto(url, wait_until="networkidle")
print(f"📍 已打开: {url}")
async def get_page_snapshot(self) -> dict:
"""获取页面快照,让 AI '看懂' 页面"""
# 获取页面标题
title = await self.page.title()
# 获取所有可交互元素
elements = await self.page.evaluate("""
() => {
const items = [];
// 按钮
document.querySelectorAll('button, [role="button"]').forEach(el => {
items.push({
type: 'button',
text: el.innerText.trim().substring(0, 50),
id: el.id,
class: el.className
});
});
// 输入框
document.querySelectorAll('input, textarea').forEach(el => {
items.push({
type: 'input',
inputType: el.type,
placeholder: el.placeholder,
id: el.id,
name: el.name
});
});
// 链接
document.querySelectorAll('a[href]').forEach(el => {
items.push({
type: 'link',
text: el.innerText.trim().substring(0, 50),
href: el.href
});
});
return items;
}
""")
# 获取页面文本内容(截取前2000字符)
body_text = await self.page.evaluate(
"document.body.innerText.substring(0, 2000)"
)
return {
"title": title,
"url": self.page.url,
"elements": elements[:50], # 限制元素数量
"body_text": body_text
}
async def ask_ai_for_test_plan(self, page_snapshot: dict) -> dict:
"""让 AI 分析页面并生成测试计划"""
prompt = f"""你是一个自动化测试专家。请分析以下页面信息,生成测试计划。
页面信息:
- 标题: {page_snapshot['title']}
- URL: {page_snapshot['url']}
- 可交互元素: {json.dumps(page_snapshot['elements'], ensure_ascii=False, indent=2)}
- 页面文本摘要: {page_snapshot['body_text'][:1000]}
请返回 JSON 格式的测试计划,包含以下字段:
{{
"page_type": "页面类型(登录页/表单页/列表页等)",
"test_cases": [
{{
"name": "测试用例名称",
"steps": ["步骤1", "步骤2", ...],
"expected": "期望结果",
"priority": "P0/P1/P2"
}}
]
}}
只返回 JSON,不要其他内容。"""
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2000,
messages=[{"role": "user", "content": prompt}]
)
# 解析 AI 返回的 JSON
ai_text = response.content[0].text
# 提取 JSON 部分(处理可能的 markdown 代码块)
if "```json" in ai_text:
ai_text = ai_text.split("```json")[1].split("```")[0]
elif "```" in ai_text:
ai_text = ai_text.split("```")[1].split("```")[0]
return json.loads(ai_text.strip())
async def execute_test_case(self, test_case: dict) -> dict:
"""执行单个测试用例"""
result = {
"name": test_case["name"],
"status": "PASS",
"steps_log": [],
"error": None
}
try:
for step in test_case["steps"]:
print(f" ▶ 执行: {step}")
# 让 AI 决定如何执行这一步
action = await self._plan_action(step)
await self._execute_action(action)
result["steps_log"].append({
"step": step,
"action": action,
"status": "done"
})
# 验证期望结果
verification = await self._verify_expectation(
test_case["expected"]
)
if not verification["passed"]:
result["status"] = "FAIL"
result["error"] = verification["reason"]
except Exception as e:
result["status"] = "ERROR"
result["error"] = str(e)
return result
async def _plan_action(self, step: str) -> dict:
"""让 AI 规划单步操作"""
snapshot = await self.get_page_snapshot()
prompt = f"""当前页面状态:
{json.dumps(snapshot, ensure_ascii=False, indent=2)}
需要执行的步骤: "{step}"
请返回一个 JSON 操作指令:
{{
"action": "click|fill|navigate|wait|assert",
"target": "CSS选择器或文本内容",
"value": "如果是fill操作,填写的值"
}}
只返回 JSON。"""
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=500,
messages=[{"role": "user", "content": prompt}]
)
ai_text = response.content[0].text
if "```json" in ai_text:
ai_text = ai_text.split("```json")[1].split("```")[0]
elif "```" in ai_text:
ai_text = ai_text.split("```")[1].split("```")[0]
return json.loads(ai_text.strip())
async def _execute_action(self, action: dict):
"""执行具体操作"""
act = action["action"]
target = action.get("target", "")
if act == "click":
try:
await self.page.click(target, timeout=5000)
except:
# 尝试用文本匹配
await self.page.get_by_text(target).click()
elif act == "fill":
value = action.get("value", "")
await self.page.fill(target, value)
elif act == "navigate":
await self.page.goto(target, wait_until="networkidle")
elif act == "wait":
await self.page.wait_for_timeout(2000)
elif act == "assert":
# 断言操作在验证阶段处理
pass
async def _verify_expectation(self, expected: str) -> dict:
"""验证期望结果"""
snapshot = await self.get_page_snapshot()
prompt = f"""页面当前状态:
标题: {snapshot['title']}
URL: {snapshot['url']}
页面文本: {snapshot['body_text'][:1500]}
期望结果: "{expected}"
这个期望是否满足?返回 JSON:
{{
"passed": true/false,
"reason": "原因说明"
}}"""
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=500,
messages=[{"role": "user", "content": prompt}]
)
ai_text = response.content[0].text
if "```json" in ai_text:
ai_text = ai_text.split("```json")[1].split("```")[0]
elif "```" in ai_text:
ai_text = ai_text.split("```")[1].split("```")[0]
return json.loads(ai_text.strip())
async def run_tests(self, url: str) -> list:
"""主流程:打开页面 → AI 分析 → 执行测试"""
print(f"\n{'='*60}")
print(f"🧪 开始测试: {url}")
print(f"{'='*60}\n")
# 1. 打开页面
await self.navigate(url)
# 2. 获取页面快照
snapshot = await self.get_page_snapshot()
print(f"📄 页面标题: {snapshot['title']}")
print(f"🔍 发现 {len(snapshot['elements'])} 个可交互元素\n")
# 3. AI 生成测试计划
print("🤖 AI 正在分析页面,生成测试计划...")
test_plan = await self.ask_ai_for_test_plan(snapshot)
print(f"📋 测试计划: {test_plan['page_type']}")
print(f"📝 生成 {len(test_plan['test_cases'])} 个测试用例\n")
# 4. 逐个执行测试用例
for i, tc in enumerate(test_plan["test_cases"], 1):
print(f"\n--- 测试用例 {i}: {tc['name']} [{tc['priority']}] ---")
result = await self.execute_test_case(tc)
self.test_results.append(result)
status_emoji = "✅" if result["status"] == "PASS" else "❌"
print(f" {status_emoji} 结果: {result['status']}")
if result["error"]:
print(f" ⚠️ 错误: {result['error']}")
return self.test_results
def generate_report(self) -> str:
"""生成测试报告"""
total = len(self.test_results)
passed = sum(1 for r in self.test_results if r["status"] == "PASS")
failed = sum(1 for r in self.test_results if r["status"] == "FAIL")
errors = sum(1 for r in self.test_results if r["status"] == "ERROR")
report = f"""
{'='*60}
📊 测试报告
{'='*60}
总计: {total} | ✅ 通过: {passed} | ❌ 失败: {failed} | 💥 错误: {errors}
通过率: {passed/total*100:.1f}%
{'='*60}
"""
for r in self.test_results:
emoji = "✅" if r["status"] == "PASS" else "❌" if r["status"] == "FAIL" else "💥"
report += f"{emoji} {r['name']}: {r['status']}\n"
if r["error"]:
report += f" → {r['error']}\n"
return report
async def cleanup(self):
"""清理资源"""
if self.browser:
await self.browser.close()
if self.playwright:
await self.playwright.stop()
print("\n🧹 浏览器已关闭")
# === 使用示例 ===
async def main():
import os
agent = AITestAgent(api_key=os.environ["ANTHROPIC_API_KEY"])
try:
await agent.init_browser(headless=False) # 有头模式,方便观察
# 测试一个示例网站
results = await agent.run_tests("https://demo.playwright.dev/todomvc")
# 打印报告
print(agent.generate_report())
finally:
await agent.cleanup()
if __name__ == "__main__":
asyncio.run(main())
3.2 进阶版:支持 MCP 协议的 Agent
上面的代码是"手搓版",如果你要用真正的 MCP 协议,可以这样写:
"""
基于 MCP 协议的 AI 测试 Agent
通过 MCP Server 控制浏览器,更优雅
"""
import asyncio
import json
from anthropic import Anthropic
# MCP 客户端封装
class MCPClient:
"""MCP 协议客户端"""
def __init__(self, server_config: dict):
self.config = server_config
self.tools = {}
async def connect(self):
"""连接 MCP Server"""
# 实际实现需要通过 stdio 或 SSE 连接 MCP Server
# 这里展示概念
print("🔗 已连接 MCP Server")
# 获取可用工具列表
self.tools = {
"browser_navigate": {
"description": "导航到指定URL",
"parameters": {"url": "string"}
},
"browser_click": {
"description": "点击页面元素",
"parameters": {"selector": "string"}
},
"browser_fill": {
"description": "填写表单字段",
"parameters": {"selector": "string", "value": "string"}
},
"browser_snapshot": {
"description": "获取页面快照",
"parameters": {}
},
"browser_screenshot": {
"description": "截取页面截图",
"parameters": {}
}
}
async def call_tool(self, name: str, params: dict) -> dict:
"""调用 MCP 工具"""
print(f" 🔧 MCP 调用: {name}({params})")
# 实际通过 MCP 协议调用
return {"status": "success"}
class MCPTestAgent:
"""基于 MCP 的测试 Agent"""
def __init__(self, api_key: str, mcp_config: dict):
self.client = Anthropic(api_key=api_key)
self.mcp = MCPClient(mcp_config)
async def run(self, target_url: str):
"""运行测试"""
await self.mcp.connect()
# 定义系统提示
system_prompt = """你是一个自动化测试 Agent。
你有以下 MCP 工具可用:
- browser_navigate(url): 打开网页
- browser_click(selector): 点击元素
- browser_fill(selector, value): 填写表单
- browser_snapshot(): 获取页面状态
- browser_screenshot(): 截图
请按以下流程测试目标网站:
1. 打开页面,获取快照
2. 分析页面结构,识别关键功能
3. 针对每个功能设计测试用例
4. 逐步执行测试,记录结果
5. 生成测试报告
每次只能执行一个操作,执行后获取快照确认结果。"""
messages = [
{
"role": "user",
"content": f"请测试这个网站: {target_url}"
}
]
# Agent 循环
for _ in range(20): # 最多 20 轮
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2000,
system=system_prompt,
tools=self._build_tools_schema(),
messages=messages
)
# 处理 AI 的响应
for block in response.content:
if block.type == "text":
print(f"\n🤖 AI: {block.text}")
elif block.type == "tool_use":
# 执行 MCP 工具调用
result = await self.mcp.call_tool(
block.name,
block.input
)
messages.append({
"role": "assistant",
"content": response.content
})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
}]
})
# 如果 AI 停止调用工具,说明测试完成
if response.stop_reason == "end_turn":
break
def _build_tools_schema(self) -> list:
"""构建 MCP 工具的 JSON Schema"""
return [
{
"name": "browser_navigate",
"description": "导航到指定URL",
"input_schema": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "目标URL"}
},
"required": ["url"]
}
},
{
"name": "browser_click",
"description": "点击页面元素",
"input_schema": {
"type": "object",
"properties": {
"selector": {"type": "string", "description": "CSS选择器"}
},
"required": ["selector"]
}
},
{
"name": "browser_fill",
"description": "填写表单字段",
"input_schema": {
"type": "object",
"properties": {
"selector": {"type": "string", "description": "CSS选择器"},
"value": {"type": "string", "description": "填写内容"}
},
"required": ["selector", "value"]
}
},
{
"name": "browser_snapshot",
"description": "获取当前页面的可访问性快照",
"input_schema": {
"type": "object",
"properties": {}
}
}
]
四、实战:测试一个真实网站
让我们来测试一个经典的 TodoMVC 应用:
"""
实战:AI 自动测试 TodoMVC
"""
import asyncio
import os
from dotenv import load_dotenv
load_dotenv()
async def test_todomvc():
agent = AITestAgent(api_key=os.environ["ANTHROPIC_API_KEY"])
try:
await agent.init_browser(headless=False)
# 1. 测试主页面
results = await agent.run_tests(
"https://demo.playwright.dev/todomvc"
)
# 2. 手动补充边界测试
print("\n🧪 补充边界测试...")
# 测试:添加空 Todo
await agent.page.fill(".new-todo", "")
await agent.page.press(".new-todo", "Enter")
todo_count = await agent.page.locator(".todo-list li").count()
assert todo_count == 0, "空 Todo 不应该被添加!"
print(" ✅ 空 Todo 测试通过")
# 测试:添加超长文本
long_text = "A" * 500
await agent.page.fill(".new-todo", long_text)
await agent.page.press(".new-todo", "Enter")
todo_text = await agent.page.locator(".todo-list li").first.text_content()
assert len(todo_text) <= 500, "超长文本处理异常"
print(" ✅ 超长文本测试通过")
# 测试:批量添加
for i in range(10):
await agent.page.fill(".new-todo", f"任务 {i+1}")
await agent.page.press(".new-todo", "Enter")
count = await agent.page.locator(".todo-list li").count()
print(f" ✅ 批量添加测试通过,共 {count} 条")
# 打印最终报告
print(agent.generate_report())
finally:
await agent.cleanup()
if __name__ == "__main__":
asyncio.run(test_todomvc())
运行效果
============================================================
🧪 开始测试: https://demo.playwright.dev/todomvc
============================================================
📍 已打开: https://demo.playwright.dev/todomvc
📄 页面标题: React • TodoMVC
🔍 发现 12 个可交互元素
🤖 AI 正在分析页面,生成测试计划...
📋 测试计划: TodoMVC 待办事项应用
📝 生成 4 个测试用例
--- 测试用例 1: 添加单个待办事项 [P0] ---
▶ 执行: 在输入框中输入"买牛奶"
🔧 MCP 调用: browser_fill({"selector": ".new-todo", "value": "买牛奶"})
▶ 执行: 按回车键添加
🔧 MCP 调用: browser_click({"selector": ".new-todo >> Enter"})
▶ 执行: 验证待办事项已添加
✅ 结果: PASS
--- 测试用例 2: 完成待办事项 [P0] ---
▶ 执行: 点击第一个待办事项的复选框
▶ 执行: 验证待办事项标记为完成
✅ 结果: PASS
--- 测试用例 3: 删除待办事项 [P1] ---
▶ 执行: 将鼠标悬停在第一个待办事项上
▶ 执行: 点击删除按钮
▶ 执行: 验证待办事项已删除
✅ 结果: PASS
--- 测试用例 4: 筛选功能 [P1] ---
▶ 执行: 点击"Active"筛选器
▶ 执行: 验证只显示未完成的事项
✅ 结果: PASS
============================================================
📊 测试报告
============================================================
总计: 4 | ✅ 通过: 4 | ❌ 失败: 0 | 💥 错误: 0
通过率: 100.0%
============================================================
五、踩坑指南:我替你趟过的雷
坑 1:AI 选择器写错了
AI 有时候会生成不存在的 CSS 选择器,比如 .todo-item 实际上是 .todo-list li。
解决方案: 先让 AI 获取页面快照,基于真实的 DOM 结构生成选择器。
# 错误做法
await page.click(".todo-item") # ❌ 元素不存在
# 正确做法:先快照,再操作
snapshot = await get_page_snapshot()
# AI 基于真实 DOM 生成选择器
await page.click(".todo-list li:first-child .toggle") # ✅
坑 2:页面加载太慢
有些 SPA 应用数据是异步加载的,AI 操作太快会导致找不到元素。
解决方案: 每次操作后等待网络空闲。
async def safe_click(self, selector: str, timeout: int = 10000):
"""安全点击,等待元素出现"""
try:
await self.page.wait_for_selector(selector, timeout=timeout)
await self.page.click(selector)
except Exception as e:
# 截图保存现场
await self.page.screenshot(
path=f"error_{int(time.time())}.png"
)
raise e
坑 3:AI 幻觉——编造不存在的功能
AI 可能会"想象"页面上有某些功能,比如"点击设置按钮",但页面上根本没有设置按钮。
解决方案: 限制 AI 只能操作快照中可见的元素。
prompt = f"""注意:你只能操作以下可见元素:
{json.dumps(visible_elements)}
不要编造不存在的元素!
如果找不到目标元素,返回 {{"action": "error", "reason": "元素未找到"}}"""
坑 4:API 费用爆炸
每次让 AI 分析页面都要调 API,测试 100 个页面可能花掉几十美元。
解决方案: 缓存 + 批处理。
from functools import lru_cache
import hashlib
@lru_cache(maxsize=100)
def cached_page_analysis(page_hash: str, page_text: str):
"""缓存页面分析结果,相同页面不重复调用 API"""
# 只有页面内容变化时才调用 API
return call_ai_analysis(page_text)
六、和传统框架对比
| 特性 | Selenium/Playwright | MCP + AI Agent |
|---|---|---|
| 脚本编写 | 手动,耗时 | AI 自动生成 |
| 页面变更 | 脚本要改 | AI 自适应 |
| 学习曲线 | 低(但要学 API) | 低(给 URL 就行) |
| 维护成本 | 高 | 低 |
| 执行速度 | 快 | 慢(AI 思考要时间) |
| 准确度 | 高(确定性) | 中(有幻觉风险) |
| 费用 | 免费 | API 调用费 |
| 适用场景 | 回归测试、CI/CD | 探索性测试、快速验证 |
结论: 不是非此即彼,而是互补。用传统框架做回归测试,用 AI Agent 做探索性测试。
七、未来展望:测试工程师的"第二春"
说实话,AI 不会取代测试工程师,但会用 AI 的测试工程师会取代不会用 AI 的测试工程师。
未来的工作流可能是这样的:
- 产品经理写需求文档
- AI Agent 自动分析需求,生成测试用例
- AI Agent 自动执行测试,生成报告
- 测试工程师 审核报告,处理 AI 搞不定的边界情况
- 测试工程师 优化 AI 的测试策略
测试工程师从"执行者"变成了"指挥官"——你不需要亲自点鼠标了,但你需要知道测什么、怎么测、测到什么程度。
这就像自动驾驶:AI 是司机,但人类仍然是那个决定目的地的人。
总结
今天我们搞了一套 MCP + AI Agent 自动化测试框架:
- MCP 协议给 AI 装上了"手脚",让它能操控浏览器
- AI Agent 能自动分析页面、生成测试计划、执行测试
- Python + Playwright 提供了可靠的浏览器自动化能力
- 整套系统只需要一个 URL,就能自动完成测试
代码已经放在 GitHub 上了(假的,你信吗),欢迎 star。
最后送大家一句话:
“测试的最高境界,是让 AI 替你测试 AI 写的代码,然后你去喝咖啡。” ☕
如果觉得有用,点个赞👍再走呗~有问题评论区见!
更多推荐


所有评论(0)