1. 项目概述当UI自动化测试遇上MCP最近在搞一个项目需要频繁地对一个Web应用的前端界面进行回归测试。手动点来点去不仅效率低还容易漏测。一开始我考虑用传统的Selenium但后来发现Playwright这个后起之秀在稳定性和功能上更胜一筹。更让我感兴趣的是社区里开始讨论如何将Playwright与MCPModel Context Protocol结合实现更“智能”的自动化测试。这听起来有点抽象简单来说MCP就像给自动化测试脚本装上一个“大脑”让它能理解测试意图甚至动态调整测试策略而不仅仅是机械地执行预设步骤。这正好切中了当前AI赋能自动化的热点。所以我决定深入折腾一下“使用Playwright MCP实现UI自动化测试”这个组合看看它到底能带来什么不一样的效率提升和可能性。这个方案的核心价值在于它试图解决传统UI自动化测试的几个老大难问题一是测试脚本脆弱前端UI稍有改动比如一个按钮的># 创建项目目录 mkdir playwright-mcp-demo cd playwright-mcp-demo # 初始化npm项目 npm init -y # 安装Playwright测试运行器及相关库 npm install playwright/test # 安装Playwright浏览器建议安装避免使用系统全局浏览器可能带来的版本问题 npx playwright install chromium接下来初始化Playwright配置文件playwright.config.ts。这里我强烈推荐使用TypeScript它能提供更好的类型提示减少低级错误。import { defineConfig, devices } from playwright/test; export default defineConfig({ testDir: ./tests, // 测试用例存放目录 fullyParallel: true, // 是否完全并行运行测试 forbidOnly: !!process.env.CI, // 在CI环境中禁止使用test.only retries: process.env.CI ? 2 : 0, // CI环境下失败重试2次 workers: process.env.CI ? 1 : undefined, // CI环境下使用1个worker本地可根据CPU核心数设置 reporter: html, // 使用HTML报告非常直观 use: { baseURL: http://localhost:3000, // 你的被测应用地址 trace: on-first-retry, // 首次失败时记录追踪信息便于调试 screenshot: only-on-failure, // 仅在失败时截图 video: retain-on-failure, // 仅在失败时保留录像 }, projects: [ { name: chromium, use: { ...devices[Desktop Chrome] }, }, // 可以添加更多项目如 Firefox, WebKit ], });3.2 MCP Server的选择与搭建这是整个方案中最关键也最灵活的部分。你需要一个MCP Server来桥接AI模型和你的Playwright环境。目前有几种路径使用开源MCP Server社区已经有一些针对Playwright的开源MCP Server项目。你可以搜索“playwright-mcp-server”之类的关键词。这些项目通常提供了一个基础的Server暴露了如goto,click,get_text等工具。优点是开箱即用缺点是可能无法完全满足你的定制化需求。自行开发MCP Server这是我最推荐的方式因为你可以完全控制暴露的工具集和逻辑。MCP协议基于JSON-RPC并不复杂。你可以用Node.js、Python等快速搭建。我选择用Node.js自行开发因为它与Playwright测试栈语言一致集成更顺畅。核心是使用modelcontextprotocol/sdk这个官方SDK。首先安装SDKnpm install modelcontextprotocol/sdk然后创建一个简单的Server (server/mcp-server.js)const { Server } require(modelcontextprotocol/sdk/server/index.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const { playwrightTools } require(./playwright-tools.js); // 这是我们自定义Playwright工具的地方 async function main() { const server new Server( { name: playwright-mcp-server, version: 0.1.0, }, { capabilities: { tools: {}, }, } ); // 注册我们自定义的Playwright工具 server.setRequestHandler(tools/list, async () ({ tools: Object.values(playwrightTools).map(tool ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema, })), })); server.setRequestHandler(tools/call, async (request) { const tool playwrightTools[request.params.name]; if (!tool) { throw new Error(Tool ${request.params.name} not found); } // 执行工具对应的函数并返回结果 const result await tool.handler(request.params.arguments || {}); return { content: [ { type: text, text: JSON.stringify(result, null, 2), }, ], }; }); const transport new StdioServerTransport(); await server.connect(transport); console.error(Playwright MCP Server running on stdio); } main().catch((error) { console.error(Server error:, error); process.exit(1); });3.3 定义核心Playwright工具集上面的Server依赖一个关键的playwright-tools.js文件这里定义了AI模型可以调用的具体工具。这是体现你测试智能化的核心。// server/playwright-tools.js const { chromium } require(playwright); // 引入playwright let browser null; let page null; // 初始化浏览器和页面工具 const initBrowser { name: init_browser, description: 启动一个无头浏览器并创建一个新页面, inputSchema: { type: object, properties: { headless: { type: boolean, description: 是否无头模式, default: true } } }, handler: async ({ headless true }) { browser await chromium.launch({ headless }); const context await browser.newContext(); page await context.newPage(); return { success: true, message: Browser launched in ${headless ? headless : headed} mode. }; } }; // 导航到指定URL const gotoUrl { name: goto, description: 导航到指定的URL, inputSchema: { type: object, properties: { url: { type: string, description: 要导航到的完整URL } }, required: [url] }, handler: async ({ url }) { if (!page) return { error: Page not initialized. Call init_browser first. }; const response await page.goto(url); return { success: true, url: page.url(), status: response?.status() }; } }; // 根据选择器获取元素文本 const getElementText { name: get_text, description: 获取页面指定选择器元素的文本内容, inputSchema: { type: object, properties: { selector: { type: string, description: CSS选择器或Playwright定位器 } }, required: [selector] }, handler: async ({ selector }) { if (!page) return { error: Page not initialized. }; try { const text await page.textContent(selector); return { success: true, text: text || }; } catch (error) { return { error: Failed to get text: ${error.message} }; } } }; // 点击元素 const clickElement { name: click, description: 点击页面上的指定元素, inputSchema: { type: object, properties: { selector: { type: string, description: CSS选择器或Playwright定位器 } }, required: [selector] }, handler: async ({ selector }) { if (!page) return { error: Page not initialized. }; try { await page.click(selector); return { success: true, message: Clicked element: ${selector} }; } catch (error) { return { error: Failed to click: ${error.message} }; } } }; // 在输入框填充文本 const fillInput { name: fill, description: 向指定的输入框填充文本, inputSchema: { type: object, properties: { selector: { type: string, description: 输入框的选择器 }, text: { type: string, description: 要输入的文本 } }, required: [selector, text] }, handler: async ({ selector, text }) { if (!page) return { error: Page not initialized. }; try { await page.fill(selector, text); return { success: true, message: Filled ${selector} with: ${text} }; } catch (error) { return { error: Failed to fill: ${error.message} }; } } }; // 关闭浏览器 const closeBrowser { name: close_browser, description: 关闭浏览器实例, inputSchema: { type: object, properties: {} }, handler: async () { if (browser) { await browser.close(); browser null; page null; } return { success: true, message: Browser closed. }; } }; // 导出所有工具 exports.playwrightTools { [initBrowser.name]: initBrowser, [gotoUrl.name]: gotoUrl, [getElementText.name]: getElementText, [clickElement.name]: clickElement, [fillInput.name]: fillInput, [closeBrowser.name]: closeBrowser, };这个工具集虽然基础但已经涵盖了UI自动化的核心操作启动、导航、查找、交互。你可以根据需要扩展更多工具如screenshot截图、wait_for_selector等待元素、evaluate执行页面脚本等。3.4 连接AI客户端搭建好MCP Server后需要让AI客户端能够连接它。以Claude Desktop或支持MCP的IDE插件如Cursor为例通常需要在其配置文件中添加你的Server。配置方式因客户端而异但核心是指定Server的启动命令。例如在Claude Desktop的配置中~/Library/Application Support/Claude/claude_desktop_config.jsonon Mac{ mcpServers: { playwright: { command: node, args: [/absolute/path/to/your/project/server/mcp-server.js], env: { NODE_ENV: development } } } }配置完成后重启AI客户端它就能发现并使用你定义的Playwright工具了。你可以直接对AI说“请使用playwright工具打开浏览器导航到https://example.com然后获取页面标题。” AI会通过MCP协议调用相应的工具并返回结果。4. 实战从零构建一个智能登录测试用例理论讲完了我们来点实际的。假设我们要测试一个经典的用户登录页面我们将演示如何利用搭建好的Playwright MCP环境来辅助我们创建、执行甚至“智能”判断一个登录测试。4.1 传统Playwright测试脚本编写首先我们看看如果不借助MCP一个标准的Playwright登录测试用例怎么写 (tests/login.spec.ts)import { test, expect } from playwright/test; test(用户成功登录, async ({ page }) { // 1. 导航到登录页 await page.goto(/login); // 2. 填写用户名和密码 await page.fill(input[nameusername], testuser); await page.fill(input[namepassword], securepassword123); // 3. 点击登录按钮 await page.click(button[typesubmit]); // 4. 断言登录成功后应跳转到仪表盘且页面包含欢迎语 await expect(page).toHaveURL(/dashboard); await expect(page.locator(h1)).toContainText(欢迎回来testuser); }); test(用户使用错误密码登录失败, async ({ page }) { await page.goto(/login); await page.fill(input[nameusername], testuser); await page.fill(input[namepassword], wrongpassword); await page.click(button[typesubmit]); // 断言应停留在登录页并显示错误提示 await expect(page).toHaveURL(/login); await expect(page.locator(.alert-error)).toContainText(用户名或密码错误); });这个脚本很清晰但对于AI来说它只是一段静态代码。如果登录页的HTML结构变了比如选择器变了这个脚本就会失败需要人工去检查和修改。4.2 利用MCP辅助生成与维护脚本现在我们切换到AI助手已配置好MCP Server的视角。我们可以用自然语言与之交互你“我正在测试一个登录页面地址是http://localhost:3000/login。页面上应该有两个输入框一个叫用户名一个叫密码还有一个提交按钮。请帮我用Playwright写一个测试用testuser和securepassword123登录并验证是否跳转到了/dashboard页面。”AI通过MCP工具探索页面后“好的我先启动浏览器并打开那个页面看看……调用init_browser,goto。我看到页面了。我来找找输入框……调用get_text或通过其他方式分析。找到了用户名输入框的选择器是#username密码框是#password按钮是button:has-text(登录)。根据这些信息我为你生成以下测试脚本”import { test, expect } from playwright/test; test(AI辅助生成的登录测试, async ({ page }) { await page.goto(http://localhost:3000/login); await page.fill(#username, testuser); await page.fill(#password, securepassword123); await page.click(button:has-text(登录)); await expect(page).toHaveURL(/dashboard); });你看AI通过实时探索页面生成了基于当前实际页面结构的脚本。如果下周前端开发把#username改成了.email-input你可以再次让AI“看看现在的登录页更新一下测试脚本”。AI会重新探索生成新的选择器维护成本显著降低。4.3 实现“智能”断言与异常处理传统断言是硬编码的比如检查特定的文本。我们可以设计一个MCP工具让AI来进行更“智能”的结果验证。在playwright-tools.js中增加一个工具// 智能验证页面状态 const validatePageState { name: validate_state, description: 根据自然语言描述验证当前页面状态是否合理, inputSchema: { type: object, properties: { description: { type: string, description: 对预期状态的描述例如登录成功后应跳转到仪表盘并显示用户欢迎信息 }, currentUrl: { type: string, description: 可选的当前URL用于辅助判断 } }, required: [description] }, handler: async ({ description, currentUrl }) { if (!page) return { error: Page not initialized. }; const url currentUrl || page.url(); const pageTitle await page.title(); const mainContent await page.textContent(body); // 这里是一个简化版的逻辑。实际上你可以 // 1. 将description、url、pageTitle、mainContent一起发送给一个AI API如OpenAI进行判断。 // 2. 或者实现一些基于规则的简单逻辑。 // 本例中我们模拟一个简单的规则判断。 let isValid false; let reason ; if (description.includes(登录成功) description.includes(仪表盘)) { if (url.includes(/dashboard) mainContent.includes(欢迎)) { isValid true; reason URL包含/dashboard且页面内容包含“欢迎”字样符合登录成功状态。; } else { isValid false; reason 登录后未到达预期状态。当前URL: ${url}, 页面内容未找到明确的欢迎信息。; } } else if (description.includes(登录失败)) { if (url.includes(/login) (mainContent.includes(错误) || mainContent.includes(无效))) { isValid true; reason 停留在登录页且页面包含错误提示符合登录失败状态。; } else { isValid false; reason 异常状态。当前URL: ${url}。; } } else { reason 无法根据描述进行确定性验证建议检查断言描述或手动验证。; } return { success: true, validation: { description, isValid, reason, observed: { url, pageTitle, contentSnippet: mainContent.substring(0, 200) } // 只返回前200字符 } }; } };然后在测试脚本中我们可以不再使用硬编码的expect而是调用这个工具import { test } from playwright/test; // 假设我们有一个方法来调用MCP Server的validate_state工具 import { callMCPServer } from ../utils/mcp-client; test(使用智能验证的登录测试, async ({ page }) { await page.goto(/login); await page.fill(#username, testuser); await page.fill(#password, securepassword123); await page.click(button[typesubmit]); // 等待一下导航或状态更新 await page.waitForLoadState(networkidle); // 调用智能验证工具 const validationResult await callMCPServer(validate_state, { description: 用户使用正确凭证登录后应成功跳转到个人仪表盘页面并看到欢迎信息。, currentUrl: page.url() }); if (!validationResult.validation.isValid) { throw new Error(智能验证失败: ${validationResult.validation.reason}); } // 如果验证通过则测试成功 });这样断言逻辑变得更灵活。即使欢迎语从“欢迎回来”变成了“您好”只要AI能理解这是成功的信号测试依然可以通过。这增强了测试脚本的健壮性。5. 高级应用构建一个简单的自主测试代理我们可以更进一步尝试构建一个能理解“用户故事”并自主执行测试的简单代理。这个代理本质上是一个脚本它接收高级指令通过MCP Server调度Playwright工具并利用一个LLM如通过OpenAI API来做出决策。5.1 系统架构设计这个自主代理包含几个部分指令解析器将自然语言用户故事如“测试新用户注册流程”转化为一个结构化任务列表。状态感知器通过MCP工具如get_text,screenshot获取当前页面信息。决策大脑一个LLM根据当前状态和任务目标决定下一步执行哪个MCP工具click,fill,goto等。执行器调用决策大脑选定的MCP工具。循环控制器重复“感知-决策-执行”循环直到任务完成或失败。5.2 核心代码实现示例这里是一个极度简化的概念验证代码 (autonomous-agent.js)使用OpenAI API作为决策大脑const OpenAI require(openai); const { callTool } require(./mcp-client); // 一个封装好的调用本地MCP Server的工具函数 const openai new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // 可用的工具列表需要与MCP Server暴露的工具一致 const availableTools [ { name: goto, description: 导航到URL }, { name: get_text, description: 获取元素文本 }, { name: click, description: 点击元素 }, { name: fill, description: 填写输入框 }, { name: validate_state, description: 验证页面状态 }, ]; async function autonomousTestAgent(userStory) { console.log(开始执行用户故事: ${userStory}); // 初始状态在起点 let currentState 任务: ${userStory}\n当前状态: 浏览器已启动位于about:blank页面。; let maxSteps 20; // 防止无限循环 let step 0; while (step maxSteps) { step; console.log(\n--- 第 ${step} 步 ---); console.log(当前状态摘要: ${currentState.substring(0, 150)}...); // 1. 决策询问LLM下一步该做什么 const decision await askLLMForNextAction(currentState, availableTools); console.log(AI决策: ${decision.action} (工具: ${decision.tool})); // 如果LLM认为任务完成或无法继续 if (decision.action TASK_COMPLETE) { console.log(AI判断任务已完成。); break; } if (decision.action TASK_FAILED) { console.log(AI判断任务失败: , decision.reason); break; } // 2. 执行调用对应的MCP工具 let toolResult; try { toolResult await callTool(decision.tool, decision.parameters); console.log(工具执行结果: ${JSON.stringify(toolResult).substring(0, 100)}...); } catch (error) { toolResult { error: error.message }; console.log(工具执行出错: ${error.message}); } // 3. 更新状态将执行结果融入当前状态供下一步决策 currentState \n\n步骤${step}: 执行了[${decision.tool}]参数为${JSON.stringify(decision.parameters)}。结果: ${JSON.stringify(toolResult)}。; // 简单延迟避免操作过快 await new Promise(resolve setTimeout(resolve, 1000)); } if (step maxSteps) { console.log(达到最大步数限制任务中断。); } } async function askLLMForNextAction(state, tools) { const prompt 你是一个UI自动化测试助手。请根据当前任务状态决定下一步操作。 可用的工具列表 ${tools.map(t - ${t.name}: ${t.description}).join(\n)} 当前状态 ${state} 请以JSON格式回复包含以下字段 - action: 字符串只能是 USE_TOOL, TASK_COMPLETE, TASK_FAILED 之一。 - tool: 字符串如果action是USE_TOOL则填写要使用的工具名称。 - parameters: 对象如果action是USE_TOOL则填写调用该工具所需的参数。 - reason: 字符串简要说明做出这个决定的原因。 请确保你的决策是具体、可执行的。目标是逐步推进任务完成。 ; const response await openai.chat.completions.create({ model: gpt-4, // 或 gpt-3.5-turbo messages: [{ role: user, content: prompt }], temperature: 0.1, // 低随机性保证决策稳定 response_format: { type: json_object } }); return JSON.parse(response.choices[0].message.content); } // 启动代理测试一个简单的故事 autonomousTestAgent(打开百度首页在搜索框输入“Playwright”然后点击搜索按钮。).catch(console.error);这个代理非常初级但它展示了可能性。在实际应用中你需要设计更精细的提示词让LLM更好地理解网页结构和测试目标。提供更丰富的页面上下文给LLM比如截图通过Base64编码、DOM结构摘要等。实现更强大的错误处理和回退机制比如当click失败时尝试其他选择器。设置明确的成功/失败条件让LLM能准确判断任务状态。实操心得自主测试代理目前还远未达到生产可用的程度。它的主要价值在于探索性测试和生成测试脚本的“草稿”。我通常用它来快速遍历一个复杂的新页面生成一份大致的操作序列和元素选择器报告然后由测试工程师将其优化、固化成可靠的自动化脚本。直接让它执行关键业务的回归测试风险太高。6. 工程化实践集成到CI/CD与团队协作个人玩转之后我们需要考虑如何将这套Playwright MCP方案工程化融入团队的开发测试流程。6.1 目录结构规划一个清晰的项目结构有助于团队协作和维护。playwright-mcp-project/ ├── package.json ├── playwright.config.ts ├── tests/ # Playwright 测试用例 │ ├── login.spec.ts │ ├── checkout.spec.ts │ └── ... ├── server/ # MCP Server 相关 │ ├── mcp-server.js # MCP Server 主入口 │ ├── playwright-tools.js # Playwright 工具定义 │ └── llm-integration.js # 与外部LLM集成的逻辑如果用到 ├── utils/ │ ├── mcp-client.js # 封装调用MCP Server的客户端 │ └── autonomous-agent.js # 自主测试代理实验性 ├── pages/ # Page Object 模型 │ ├── LoginPage.ts │ └── DashboardPage.ts ├── fixtures/ # 测试夹具 ├── .github/workflows/ # GitHub Actions CI 配置 │ └── ci.yml └── .env.example # 环境变量示例6.2 与CI/CD流水线集成我们可以在GitHub Actions中这样配置CI流水线 (.github/workflows/ci.yml)name: Playwright Tests on: [push, pull_request] jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 18 - name: Install dependencies run: npm ci - name: Install Playwright Browsers run: npx playwright install --with-deps chromium - name: Run Playwright tests run: npx playwright test env: BASE_URL: ${{ secrets.BASE_URL }} # 从仓库Secrets读取测试环境地址 - uses: actions/upload-artifactv4 if: always() with: name: playwright-report path: playwright-report/ retention-days: 30这里的流水线运行的是传统的、确定的Playwright测试脚本。MCP Server和AI辅助部分目前更适合放在本地开发、脚本生成与维护阶段而不是不稳定的CI环境中。6.3 团队协作与知识沉淀共享MCP工具集将团队共同维护的、稳定的MCP工具集playwright-tools.js作为项目核心资产。当新增通用操作如处理某种特定弹窗时就将其工具化。Prompt模板库积累用于AI辅助生成脚本的Prompt模板。例如“请为[页面描述]生成一个Playwright测试脚本覆盖[功能点1]和[功能点2]使用Page Object模式。”将AI生成的脚本视为“初稿”建立团队规范所有AI生成的脚本必须经过人工审查、优化例如替换不稳定的选择器为>