
1. 项目概述从自然语言到自动化执行的桥梁最近在折腾自动化测试发现一个挺有意思的痛点测试脚本的编写和维护尤其是面对复杂业务逻辑和频繁变更的UI时依然是个体力活。我们总在追求“写更少的代码做更多的事”那能不能直接用自然语言描述测试意图然后自动生成可执行的测试脚本并运行呢听起来有点像天方夜谭但结合几个新兴的工具这事儿还真能跑通。我花了不少时间把 Playwright、Filesystem MCPModel Context Protocol以及一些自然语言处理NLP的思路揉在一起搞出了一套从“自然语言描述”到“结构化操作”再到“测试执行”和“报告输出”的全流程自动化方案。这套方案的核心就是让测试的创建和执行变得更“傻瓜”让业务、产品甚至是不太懂代码的测试同学也能快速参与进来。简单来说这个项目就是构建一个“翻译器”和“执行器”。你告诉它“我想测试用户登录功能输入正确的用户名和密码后应该跳转到首页”它就能理解你的意图将其拆解成一系列可操作的结构化指令比如打开登录页、定位用户名输入框、输入文本、定位密码输入框、输入文本、点击登录按钮、验证当前URL然后调用 Playwright 自动执行这些操作最后把成功或失败的结果连同截图、日志一起打包成一份清晰的测试报告。整个过程你只需要关心“测什么”而不用操心“怎么测”和“怎么写代码”。2. 核心思路与技术选型解析2.1 为什么是 Playwright Filesystem MCP要搭建这样一个系统我们需要几个核心组件一个强大的浏览器自动化工具一个能理解并转换自然语言的“大脑”以及一个连接两者的、灵活可扩展的“桥梁”。首先浏览器自动化工具我毫不犹豫地选择了Playwright。相比于 Selenium 或 PuppeteerPlaywright 的优势太明显了它原生支持 Chromium、Firefox 和 WebKit 三大浏览器引擎跨浏览器测试一致性高它的自动等待机制Auto-waiting极大地减少了编写显式等待的烦恼脚本更健壮API 设计非常现代和友好特别是它的locator定位策略结合文本内容、角色role等让元素定位变得直观。最重要的是Playwright 的测试运行器Test Runner和报告系统已经非常成熟我们不用从头造轮子。那么谁来把“登录”这个自然语言指令翻译成 Playwright 能懂的“page.goto(‘/login’)、page.locator(‘[name“username”]’).fill(‘admin’)”呢这就是MCPModel Context Protocol出场的时候了。MCP 本质上是一个协议它定义了大语言模型LLM与外部工具、数据源之间如何安全、结构化地交互。你可以把它想象成给 LLM 装上了一双能操作外部世界的手和眼睛。而Filesystem MCP Server是 MCP 协议的一个具体实现它允许 LLM 通过标准的 MCP 接口安全地读写本地文件系统。这听起来好像和浏览器自动化没关系但它的妙处在于我们可以利用 LLM 的理解和生成能力结合 Filesystem MCP 的文件操作能力来动态生成、修改和管理我们的测试脚本文件。也就是说LLM 是“大脑”负责理解自然语言并生成结构化操作序列Filesystem MCP 是“手”负责把这些操作序列写入到.spec.js或.py文件中最后我们再调用 Playwright 去执行这个刚刚生成的文件。2.2 整体架构与工作流拆解整个系统的运行流程可以清晰地分为四个阶段形成了一个完整的闭环自然语言输入与意图解析用户通过命令行、Web界面或聊天机器人输入一段测试需求例如“测试商品搜索功能关键词‘手机’验证结果列表包含至少一个商品”。系统调用 LLM如 GPT-4、Claude 或本地部署的模型的 API将这段描述进行解析。这里的关键是 prompt 工程我们需要引导 LLM 输出结构化的 JSON 数据而不是一段模糊的文字。结构化操作序列生成LLM 根据预设的指令将自然语言需求转换成一个由“操作步骤”组成的列表。每个步骤都是一个结构化的对象包含操作类型如navigate,click,fill,assert、目标元素定位器如‘button:has-text(“搜索”)’、操作值如输入的文本‘手机’以及可选的断言条件。这个结构化的列表是后续所有动作的蓝图。测试脚本动态构建与执行系统接收到结构化操作序列后Filesystem MCP Server 开始工作。它根据一个预设的测试脚本模板将操作序列“填充”进去生成一个完整的、可执行的 Playwright 测试文件。然后系统调用 Playwright Test Runner执行这个新生成的测试文件。Playwright 会启动浏览器严格按照脚本步骤操作并记录每一步的结果。测试结果收集与报告生成测试执行完毕后Playwright 会原生生成多种格式的报告如 HTML、JSON、JUnit。我们的系统可以捕获这些报告进行二次加工。例如将原始的 JSON 结果与最初的自然语言描述关联起来生成更业务友好的测试报告说明“针对‘商品搜索’需求的测试已执行其中‘验证结果列表’步骤通过/失败”并附上失败时的截图和错误堆栈。这个架构的灵活性在于每个环节都可以替换或增强。LLM 可以换用不同的模型Filesystem MCP 可以换成 Database MCP 来操作测试用例库Playwright 的报告也可以集成到 CI/CD 平台如 Jenkins, GitLab CI中。3. 关键技术细节与实现要点3.1 设计高效的“自然语言到结构化操作”的 Prompt这是整个系统的“智能”核心也是最容易出问题的地方。LLM 的理解能力再强如果没有清晰的指令它也可能输出混乱的结果。我们的目标是让 LLM 输出一个稳定、可解析的 JSON 数组。一个经过多次调试后比较有效的 Prompt 示例你是一个专业的Web自动化测试工程师。请将用户的需求转化为一系列可执行的Playwright操作步骤。 输出格式必须是严格的JSON数组每个对象代表一个步骤包含以下字段 - action: 操作类型。必须是以下之一[navigate, click, fill, select_option, check, uncheck, hover, wait_for_time, wait_for_selector, assert_text, assert_visible, assert_url]。 - locator: 用于定位元素的Playwright定位器字符串。尽可能使用稳定、语义化的定位方式如 getByRole(button, { name: Submit }) 或 locator(textLogin)。如果操作是navigate此字段为URL字符串。 - value: 可选。执行操作时需要的值如fill操作的文本select_option的选项值。 - assertion: 可选。仅当action为assert_*类型时使用描述断言的具体期望值如期望的文本内容。 用户需求{user_input} 请直接输出JSON数组不要有任何额外的解释。关键点与避坑经验枚举操作类型严格限定action的取值避免 LLM 发明不存在的操作导致后续脚本生成失败。推广稳定定位器在 Prompt 中鼓励使用 Playwright 推荐的getByRole、getByText等定位方式它们比脆弱的 CSS 选择器如#loginBtn更抗UI变更。可以给出例子进行引导。处理模糊描述用户可能会说“点击那个大大的按钮”。LLM 可能会生成locator(‘button:has-text(“大大的”)’)这样显然会失败的定位器。一种缓解策略是在 Prompt 中加入上下文比如提供目标页面的部分 HTML 片段可通过 MCP 先读取一个样板文件让 LLM 的定位更有依据。但这会消耗更多 Token需要权衡。后置校验生成的 JSON 必须通过程序化的 Schema 校验例如使用 Python 的 Pydantic 或 JS 的 Zod确保格式完全正确再进入下一步。不要盲目信任 LLM 的输出。3.2 利用 Filesystem MCP Server 进行文件操作Filesystem MCP Server 通常作为一个独立的服务运行通过标准输入输出stdio或 HTTP 与主程序通信。我们的主程序可以是一个 Python 脚本或 Node.js 服务需要与这个 Server 交互。核心操作无非是“读模板”和“写脚本”读取模板通过 MCP 调用read_file工具读取一个事先写好的 Playwright 测试模板文件。这个模板文件里预留了插入操作步骤的占位符例如// {{STEPS}}。生成脚本内容将上一步得到的结构化操作序列遍历并转换成对应的 Playwright 代码行。例如{“action”: “fill”, “locator”: “getByRole(‘textbox’, { name: ‘Username’ })”, “value”: “admin”}转换成await page.getByRole(‘textbox’, { name: ‘Username’ }).fill(‘admin’);。写入新文件将转换后的代码行替换掉模板中的占位符形成完整的测试脚本。然后通过 MCP 调用write_file工具将内容写入到一个新的文件中例如test_generated.spec.js。注意Filesystem MCP 的设计通常包含安全限制比如只能访问特定目录称为“沙箱”。在配置 MCP Server 时务必确保其工作目录Sandbox包含你的模板目录和允许生成测试脚本的输出目录。否则会出现“权限拒绝”的错误。实操心得不要为每一个测试需求都生成一个全新的模板。可以维护一个基础的、包含测试套件设置test.describe、前置后置操作test.beforeEach的模板。生成脚本时只替换测试用例test(‘…’, async ({ page }) { … })内部的具体步骤。这样更易于管理也符合 Playwright 的最佳实践。3.3 Playwright 测试脚本的动态执行与结果捕获生成了test_generated.spec.js文件后我们需要执行它。Playwright Test 提供了编程式 APIplaywright test作为库使用允许我们在 Node.js 或 Python 代码中启动测试运行。以 Node.js 环境为例const { exec } require(‘child_process’); const path require(‘path’); async function runGeneratedTest() { const testFilePath path.join(__dirname, ‘test_generated.spec.js’); // 使用 Playwright Test CLI 运行指定文件并指定输出报告格式和目录 const command npx playwright test ${testFilePath} --reporterhtml,json --outputtest-results/; return new Promise((resolve, reject) { exec(command, (error, stdout, stderr) { if (error) { // 测试运行本身可能出错如语法错误或测试用例失败 console.error(执行错误: ${error}); console.log(stdout: ${stdout}); console.error(stderr: ${stderr}); // 即使测试失败我们也可能成功捕获了报告所以不一定要 reject // 这里我们解析报告文件来判断最终状态 } // 无论测试通过与否stdout/stderr 都会包含信息 resolve({ stdout, stderr }); }); }); }执行完毕后关键是要去解析 Playwright 生成的报告。--reporterjson会生成一个结构化的 JSON 报告文件如test-results/results.json里面包含了每个测试的详细状态passed/failed、持续时间、错误信息、附件如截图、追踪文件路径等。我们的系统可以读取这个 JSON 文件提取关键信息与最初的自然语言需求关联形成最终的用户报告。一个重要的技巧为了能在报告中清晰区分不同会话生成的测试可以在生成测试脚本时在测试标题test(‘…’)中嵌入一个唯一标识符或原始需求的摘要例如test(‘[需求ID:123] 测试商品搜索功能’ …)。这样在查看聚合报告时一目了然。4. 端到端实现流程与代码示例让我们串联起所有环节看一个简化的、概念验证性质的 Python 实现示例。这里我们假设使用 OpenAI GPT 系列模型作为 LLM并使用一个兼容 MCP 协议的客户端库与 Filesystem MCP Server 通信。4.1 环境准备与依赖安装首先确保你的环境已经准备好安装 Playwrightpip install playwright然后playwright install安装浏览器。安装 OpenAI 库pip install openai。部署 Filesystem MCP ServerMCP Server 通常是一个可执行文件。你可以从相关项目仓库例如 Anthropic 的官方示例下载或构建。假设我们有一个名为filesystem-server的服务器程序它监听在某个端口或通过 stdio 通信。安装 MCP 客户端需要一个能与 MCP Server 通信的客户端库。例如可以使用mcp这个 Python 库如果可用或者根据 MCP 协议自行实现简单的 JSON-RPC 通信。4.2 核心模块实现我们创建几个核心的 Python 文件nlp_to_actions.py– 负责与 LLM 交互生成结构化操作序列import openai import json import os from pydantic import BaseModel, ValidationError from typing import List, Optional # 定义结构化操作步骤的数据模型 class ActionStep(BaseModel): action: str # 例如 “navigate”, “click” locator: str value: Optional[str] None assertion: Optional[str] None def parse_requirement_to_actions(user_requirement: str, api_key: str) - List[ActionStep]: openai.api_key api_key prompt f你是一个专业的Web自动化测试工程师。请将用户的需求转化为一系列可执行的Playwright操作步骤。 ...此处插入上文提到的完整Prompt... 用户需求{user_requirement} try: response openai.ChatCompletion.create( model“gpt-4”, # 或 “gpt-3.5-turbo” messages[{“role”: “user”, “content”: prompt}], temperature0.1, # 低温度输出更确定 ) # 假设LLM正确返回了纯JSON数组字符串 actions_json response.choices[0].message.content # 清理可能的 markdown 代码块标记 actions_json actions_json.strip().strip(‘’).replace(‘json\n’, ‘’) actions_list json.loads(actions_json) # 使用Pydantic进行校验和转换 validated_actions [ActionStep(**step) for step in actions_list] return validated_actions except (json.JSONDecodeError, ValidationError, KeyError) as e: print(f“解析LLM输出失败: {e}”) print(f“原始输出: {actions_json}”) return [] # 或抛出异常mcp_file_handler.py– 负责通过 MCP 读写文件import json import subprocess # 假设我们通过子进程调用一个命令行MCP客户端工具来与Server交互 class MCPFileHandler: def __init__(self, mcp_client_path, server_path): self.mcp_client_path mcp_client_path self.server_path server_path # 启动MCP Server进程示例实际通信方式可能不同 self.server_process subprocess.Popen( [server_path], stdinsubprocess.PIPE, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, textTrue ) def _send_mcp_request(self, request): 发送一个MCP协议请求并获取响应简化示例 request_str json.dumps(request) “\n” self.server_process.stdin.write(request_str) self.server_process.stdin.flush() response_line self.server_process.stdout.readline() return json.loads(response_line) def read_template(self, template_path): # 构造调用 filesystem/read_file 工具的请求 request { “jsonrpc”: “2.0”, “method”: “tools/call”, “params”: { “name”: “read_file”, “arguments”: {“path”: template_path} }, “id”: 1 } response self._send_mcp_request(request) if ‘result’ in response and ‘content’ in response[‘result’]: return response[‘result’][‘content’] else: raise Exception(f“读取模板失败: {response}”) def write_test_script(self, file_path, content): # 构造调用 filesystem/write_file 工具的请求 request { “jsonrpc”: “2.0”, “method”: “tools/call”, “params”: { “name”: “write_file”, “arguments”: {“path”: file_path, “content”: content} }, “id”: 2 } response self._send_mcp_request(request) if ‘error’ in response: raise Exception(f“写入测试脚本失败: {response[‘error’]}”) # 成功可能没有特定结果 return Trueplaywright_runner.py– 负责执行生成的测试脚本并获取结果import subprocess import json import os from pathlib import Path def run_playwright_test(test_file_path: str, output_dir: str “./test-results”): “”” 执行Playwright测试并返回结果摘要 “”” # 确保输出目录存在 Path(output_dir).mkdir(parentsTrue, exist_okTrue) # 构建命令 # 注意这里假设在项目根目录下执行且playwright已全局安装或npx可用 cmd [“npx”, “playwright”, “test”, test_file_path, “--reporterjson,html”, f“--output{output_dir}”] try: result subprocess.run( cmd, capture_outputTrue, textTrue, timeout300 # 设置超时防止卡死 ) # 无论测试用例本身成功与否命令执行完毕就继续 stdout result.stdout stderr result.stderr return_code result.returncode # 尝试读取生成的JSON报告 json_report_path os.path.join(output_dir, ‘results.json’) report_data {} if os.path.exists(json_report_path): with open(json_report_path, ‘r’, encoding‘utf-8’) as f: report_data json.load(f) return { “return_code”: return_code, “stdout”: stdout, “stderr”: stderr, “report”: report_data } except subprocess.TimeoutExpired: return {“error”: “测试执行超时”} except Exception as e: return {“error”: f“执行过程异常: {str(e)}”}main_orchestrator.py– 总控流程import sys from nlp_to_actions import parse_requirement_to_actions from mcp_file_handler import MCPFileHandler from playwright_runner import run_playwright_test import json def main(): # 1. 接收自然语言需求这里从命令行参数获取 if len(sys.argv) 2: print(“请提供测试需求例如: python main.py ‘测试登录功能’”) sys.exit(1) user_requirement sys.argv[1] # 2. 解析为结构化操作 print(“步骤1: 正在解析自然语言需求...”) openai_api_key os.getenv(“OPENAI_API_KEY”) actions parse_requirement_to_actions(user_requirement, openai_api_key) if not actions: print(“需求解析失败请检查输入或API设置。”) sys.exit(1) print(f“解析出 {len(actions)} 个操作步骤。”) # 3. 初始化MCP文件处理器 print(“步骤2: 正在连接MCP Server...”) mcp_handler MCPFileHandler(“path/to/mcp/client”, “path/to/filesystem-server”) # 4. 读取测试模板 template_content mcp_handler.read_template(“./templates/base_test.spec.js.tmpl”) # 5. 将操作步骤转换为Playwright代码行 steps_code_lines [] for step in actions: if step.action “navigate”: lines f“await page.goto(‘{step.locator}’);” elif step.action “fill”: lines f“await page.locator(‘{step.locator}’).fill(‘{step.value}’);” elif step.action “click”: lines f“await page.locator(‘{step.locator}’).click();” elif step.action “assert_text”: lines f“await expect(page.locator(‘{step.locator}’)).toHaveText(‘{step.assertion}’);” # ... 处理其他操作类型 else: continue steps_code_lines.append(lines) steps_code ‘\n ‘.join(steps_code_lines) # 6. 替换模板占位符生成完整脚本 final_script template_content.replace(‘// {{STEPS}}’, steps_code) # 7. 写入新的测试文件 generated_test_path f“./generated_tests/test_{int(time.time())}.spec.js” mcp_handler.write_test_script(generated_test_path, final_script) print(f“测试脚本已生成: {generated_test_path}”) # 8. 执行Playwright测试 print(“步骤3: 正在执行自动化测试...”) result run_playwright_test(generated_test_path) # 9. 解析并输出结果 print(“\n测试执行完成”) if “report” in result and result[“report”].get(“suites”): for suite in result[“report”][“suites”]: for test in suite.get(“tests”, []): status test[“results”][0][“status”].upper() print(f“- {test[‘title’]}: {status}”) if status “FAILED”: for attachment in test[“results”][0].get(“attachments”, []): if attachment[“name”] “screenshot”: print(f“ 失败截图: {attachment[‘path’]}”) else: print(“未能生成有效报告请检查执行日志。”) print(result.get(“stderr”, “”)) if __name__ “__main__”: main()模板文件templates/base_test.spec.js.tmplconst { test, expect } require(‘playwright/test’); test(‘Generated test from requirement’, async ({ page }) { // {{STEPS}} });这个流程跑下来你就完成了一次从自然语言到测试报告的全自动旅程。当然这是一个高度简化的原型真实系统需要考虑错误处理、重试机制、资源清理、并发执行等更多工程化问题。5. 常见问题、优化方向与避坑指南在实际搭建和运行这套系统的过程中我遇到了不少坑也总结出一些优化思路。5.1 典型问题与排查技巧问题现象可能原因排查与解决思路LLM 返回非 JSON 或格式错误Prompt 指令不清晰LLM 模型“不听话”。1. 在 Prompt 中更严格地规定格式使用“必须”、“严格”等词。2. 在代码中添加更健壮的解析和重试逻辑。3. 考虑使用 OpenAI 的 JSON Mode如果可用或 Function Calling 功能它们能更好地约束输出格式。生成的定位器无法找到元素LLM 生成的定位器不稳定或错误页面结构已变更。1. 在 Prompt 中提供更具体的定位器编写指南和示例。2. 实现一个“定位器验证”环节在生成最终脚本前先用 Playwright 快速运行一个 headless 脚本检查每个定位器在当前页面上是否存在如果失败则尝试让 LLM 重新生成或使用备用定位策略。3. 鼓励使用>Filesystem MCP 写入权限错误MCP Server 的沙箱Sandbox配置未包含目标目录。检查 MCP Server 的配置文件确保directory参数包含了你的模板目录和生成脚本的输出目录。这是 MCP 安全模型的一部分必须正确配置。Playwright 测试执行超时或失败网络慢、页面加载久、动态元素未出现生成的步骤逻辑有误如未先点击打开下拉框就选择选项。1. 在生成的代码中为关键步骤如navigate,click后添加显式等待例如await page.waitForLoadState(‘networkidle’)。2. 在转换步骤时加入更智能的逻辑。例如遇到select_option动作时检查前一步是否是打开下拉框的click如果不是则自动补上。3. 增加全局超时和重试配置。报告关联性差生成的测试标题千篇一律无法追溯原始需求。在生成测试脚本时将原始需求的哈希或摘要嵌入测试标题或描述中。例如test([Req:${reqHash}] ${user_requirement.substring(0,50)}…, …)。这样在 HTML 报告里就能清晰看到对应关系。5.2 系统优化与进阶思路引入上下文记忆Codebase Memory当前的系统是“单次对话”LLM 对被测应用AUT一无所知。可以引入一个“知识库”存储之前成功用例的步骤、页面的关键元素信息通过静态分析或爬取。在解析新需求时将这些上下文通过 MCP 提供给 LLM能极大提高定位器的准确性和步骤的合理性。实现“自我修复”循环当测试执行失败时不仅仅是报告失败。系统可以自动捕获失败时的页面截图、HTML 片段和错误信息将其作为新的上下文反馈给 LLM要求它分析失败原因并生成修复后的步骤序列然后重新执行。形成一个“生成-执行-分析-修复”的闭环。与低代码测试平台集成将这套系统的后端作为引擎前端提供一个友好的界面。用户可以在界面上用自然语言描述场景系统在后台生成并执行测试将结果可视化地展示在平台上。这比纯命令行方式更易用。技能Skill化与平台化可以将“自然语言生成 Playwright 测试”封装成一个独立的Skill。这个 Skill 可以被集成到 Claude Code、Cursor 等智能 IDE 中或者作为 Dify、LangChain 等 LLM 应用平台的一个可调用工具。用户在任何地方都能通过对话触发自动化测试的创建。降低 Token 消耗与成本频繁调用 GPT-4 成本不菲。可以考虑使用更小、更专精的本地模型经过微调来处理常见的测试模式对操作步骤进行压缩编码后再发送给 LLM缓存成功的解析结果对相似需求直接复用。5.3 最后的几点心得折腾完这一套我的感觉是这条路是通的但距离“完全替代测试工程师”还非常遥远。它更像是一个强大的“测试助手”或“效率倍增器”。它的价值在于快速原型验证产品经理有一个新想法可以立刻用自然语言描述生成一个基础的冒烟测试脚本快速验证主流程是否跑得通。解放重复劳动将那些重复、繁琐的回归测试用例编写工作自动化让测试人员更专注于探索性测试、复杂场景设计和测试策略。降低自动化门槛让业务人员也能以他们熟悉的方式说话参与到自动化测试的建设中促进团队协作。但是它也非常依赖“人”的监督和设计。Prompt 需要精心调试生成的脚本需要审查至少初期需要定位器的稳定性需要持续优化。它不是银弹而是一个需要与人的智慧紧密结合的工具。如果你正苦于自动化测试脚本的编写和维护成本不妨尝试引入 LLM 和 MCP 的思路哪怕只是实现一个最简单的需求解析器也可能为你打开一扇新的大门。