尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Pytest+Skills+MCP:让AI在约束下完成Web自动化测试

Pytest+Skills+MCP:让AI在约束下完成Web自动化测试 很多人第一次让 AI 写 Web 自动化测试时都会经历同一种挫败感提示词写得挺清楚AI 也确实生成了像模像样的代码但一跑就挂。定位不到元素、浏览器没启动、断言写错位置甚至 AI 还会一本正经地编造一个不存在的 API 名称。问题不一定出在模型能力上而是给的执行链路太松了。我的判断是现阶段做 AI 驱动的 Web 自动化重点不是“让 AI 多聪明”而是“让 AI 的每一步都被结构约束住”。Pytest Skills MCP 正是这样一个轻量组合。Pytest 负责确定性的执行与断言MCP 负责把测试能力开放给 AISkills 负责把测试规范写进 AI 的工作流。90 分钟跑通一个最小闭环完全可行。这篇文章只讲可落地的步骤。读完你可以做三件事用 Pytest 管理一套可重复执行的 Web 自动化用例用 MCP Server 暴露测试执行、技能读取等工具让 AI Agent 按照 Skills 规则调用这些工具完成一次真实回归。我不打算堆概念也不讲“AI 会取代测试工程师”这类空话。下面直接进入正题。1. 90分钟能跑通的组合解决的是什么问题先回答一个更直接的问题为什么我们需要 Pytest、Skills、MCP 三样东西凑在一起而不是单纯“让 AI 写用例”因为“让 AI 写用例”这件事本身太不可控。模型能力确实在进步但以下几点仍然是 Web 自动化的老问题元素定位不稳定。AI 生成的选择器可能上一步有效下一步就把页面上另一个重名元素选到了。执行结果缺少校验。AI 可能写出了很长的脚本但最后没有断言等于“执行了但没验证”。能力边界模糊。如果直接把浏览器控制权交给 AI它可能访问到生产环境页面或者执行了高风险操作。重复建设严重。团队里已经沉淀了很多 pytest 用例AI 与其重新造轮子不如直接调度这些既有资产。Pytest Skills MCP 的组合本质上是把三层责任分开层次角色承担什么Pytest执行与校验层用例管理、断言、结果报告、浏览器操作稳定性MCP能力连接层让 AI 能发现工具、调用工具、拿到结构化结果Skills行为约束层提前写清楚“AI 应该按什么规范和步骤来做”Agent决策编排层理解任务、拆解步骤、选择工具、观察结果并继续下一步在这个结构里AI 不再是那个“直接握着方向盘的人”而是“坐在副驾驶按照操作手册调用方向盘、刹车和油门的助手”。它负责调度Pytest 负责兜底。所以 90 分钟内我们要完成的不是做一个商业级平台而是跑通一条最小链路本地页面 → pytest 用例 → MCP Server 工具 → AI Agent 调度 → 自动回归 → 输出结果。链路通了后面所有工程化改造都有基础。2. 四个角色的分工Pytest、Skills、MCP、Agent很多人一看标题会问Pytest 是测试框架MCP 是协议Skills 是技能模块Agent 是智能体这四个东西怎么组合在一起这里先把概念边界理清楚。2.1 MCP 是什么解决什么问题MCP 的全称是 Model Context Protocol中文一般叫“模型上下文协议”。它解决的是一个大模型开发中的经典问题大模型怎么安全、统一地调用外部工具和数据源。在没有 MCP 之前每个模型接入一个工具都要写一套对接逻辑。今天接 A 公司的搜索接口明天接 B 公司的数据库后天接入内部运维平台每个接口的鉴权方式、参数格式、返回结构都不一样。AI Agent 的代码里会堆满各种定制化适配。MCP 的做法是定义一套标准服务器把工具能力暴露出来客户端负责发现和调用。模型层只需要遵循同一套协议就能动态拿到“有哪些工具、每个工具接受什么参数、会返回什么格式”然后像调用普通函数一样去调用。在 Web 自动化场景里MCP 的价值很具体你把run_pytest、read_skill这些能力注册成 MCP 工具后AI 询问“当前有哪些工具”就能看到它们不需要在提示词里手动粘贴一堆说明也不需要为每个测试框架写私有对接。这里最容易误解的点是MCP 不是一个执行引擎它不负责跑测试也不负责操作浏览器。它是一个传输和协议层。真正的测试执行仍然是 Pytest 来完成的。2.2 Skills 和 MCP 有什么区别这是最近提问率很高的问题也是新手最容易混的一点。MCP 解决的是“AI 能调用什么”Skills 解决的是“AI 应该按什么规则调用”。两者不在同一个层次。MCP 更像是一个“电话线”把 AI 和工具接通Skills 更像是一本“操作手册”告诉 AI 什么时候该用工具、用之前先做什么、哪些操作不能做、完成后要保留什么证据。举个例子。一个 Web 自动化测试 Skill 文件里通常包括允许访问的域名范围元素定位优先使用>mkdir web-auto-ai cd web-auto-ai python -m venv .venv source .venv/bin/activateWindows 下激活命令是.venv\Scripts\activate3.2 安装依赖在项目根目录创建requirements.txtpytest pytest-playwright playwright fastmcp mcp requests安装命令pip install -r requirements.txt如果下载速度慢可以切换国内镜像源例如pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple之后安装 Playwright 浏览器内核playwright install chromium这一步会下载 Chromium 到本地缓存。如果下载慢可以配置PLAYWRIGHT_DOWNLOAD_HOST指向国内镜像或者使用公司内部的统一源。不配置也不影响后面的核心代码逻辑只是首次安装时间会长一些。3.3 项目目录结构为了后续好维护建议按下面的结构组织工程web-auto-ai/ ├── demo.html ├── requirements.txt ├── tools/ │ ├── mcp_server.py │ └── mcp_client.py ├── skills/ │ └── web_auto.md ├── tests/ │ ├── conftest.py │ └── test_web.py └── reports/ └── screenshots/其中demo.html是本地被测页面tests/放普通 Pytest 测试用例tools/mcp_server.py是 MCP 服务端tools/mcp_client.py是 MCP 客户端验证脚本skills/web_auto.md是给 AI 读的规则手册。4. 第一步用 Pytest 把 Web 自动化跑通我们先用一个本地 HTML 页面作为被测目标这样不需要依赖外网环境稳定且可重现。4.1 准备被测页面在项目根目录创建demo.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleAI 测试演示页/title /head body h1AI Web 自动化演示/h1 input idtask-input placeholder输入任务 / button idsubmit-btn>from pathlib import Path import pytest from playwright.sync_api import sync_playwright pytest.fixture(scopesession) def browser(): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) yield browser browser.close() pytest.fixture def page(browser): context browser.new_context(viewport{width: 1280, height: 720}) page context.new_page() yield page context.close() pytest.fixture(autouseTrue) def ensure_reports_dir(): Path(reports/screenshots).mkdir(parentsTrue, exist_okTrue)这段代码里有两个关键设计。浏览器实例使用session作用域整个测试会话只启动一次浏览器进程避免每条用例都反复启动。每个用例则使用独立的context和page这样用例之间的登录状态、Cookie 和页面数据不会互相污染。ensure_reports_dir是自动 fixture保证截图目录一定存在避免后续截图时因为目录不存在而报错。4.3 编写测试用例创建tests/test_web.pyfrom pathlib import Path DEMO_URI Path(__file__).resolve().parent.parent.joinpath(demo.html).as_uri() def test_demo_page_interaction(page): page.goto(DEMO_URI) page.fill(#task-input, Pytest Skills MCP) page.click([data-testidsubmit]) expected_text 处理完成Pytest Skills MCP assert page.inner_text(#result) expected_text page.screenshot(pathreports/screenshots/demo_page.png)这里使用Path.as_uri()把本地文件路径转成标准的file://URL在不同操作系统下都比较稳定不用手写拼路径字符串。断言方面核心是assert page.inner_text(#result) expected_text这个断言虽然简单但它代表了自动化测试的本质不是让代码“跑一遍”而是让代码“验证一个预期结果”。4.4 运行 Pytest在项目根目录执行python -m pytest tests/test_web.py -v预期输出大致为collected 1 item tests/test_web.py::test_demo_page_interaction PASSED同时会生成reports/screenshots/demo_page.png截图。如果这一步失败常见原因包括未安装 Playwright 浏览器执行playwright install chromium浏览器启动后无法打开file://地址检查DEMO_URI是否生成正确fixture 名称写错Pytest 报fixture page not found检查conftest.py是否在tests目录下。到这里我们已经完成了 Pytest Playwright 的最小 Web 自动化闭环。它不依赖 AI但为后续步骤提供了可复用资产。5. 第二步用 MCP Server 把测试能力开放给 AI现在进入核心部分把上一步的测试能力暴露给 AI。这样模型才知道“你的工程里有哪些工具可用”而不是靠猜。5.1 实现 MCP Server创建tools/mcp_server.pyimport subprocess from pathlib import Path from fastmcp import FastMCP PROJECT_ROOT Path(__file__).resolve().parent.parent mcp FastMCP(web-test-mcp) mcp.tool() def list_test_files() - str: 列出 tests 目录下所有测试文件清单。 tests_dir PROJECT_ROOT / tests files sorted(tests_dir.glob(test_*.py)) names [f.name for f in files] if not names: return 没有找到 test_*.py 文件 return \n.join(names) mcp.tool() def run_pytest(target: str tests) - str: 运行指定测试文件、目录或用例节点的 pytest 测试。 参数 target 示例tests/test_web.py、tests、tests/test_web.py::test_demo_page_interaction 返回测试结果摘要。 result subprocess.run( [python, -m, pytest, target, -q, --tbshort], cwdPROJECT_ROOT, capture_outputTrue, textTrue, timeout180, ) std_out result.stdout[-2000:] std_err result.stderr[-2000:] if result.returncode 0: return fPASS\n{std_out} return fFAIL\n{std_out}\n{std_err} mcp.tool() def read_skill(skill_name: str web_auto) - str: 读取 skills 目录下的技能说明文件用于约束 AI 执行行为。 skill_path PROJECT_ROOT / skills / f{skill_name}.md if skill_path.exists(): return skill_path.read_text(encodingutf-8) return fSKILL_NOT_FOUND: {skill_name} if __name__ __main__: mcp.run()这里用 FastMCP 封装了三个工具。list_test_files让 AI 在执行任务前先了解项目里有哪些测试资产不需要读取整个目录树也不需要在提示词里手动维护清单。run_pytest是核心工具接受target参数支持文件、目录、用例节点三种粒度。AI 接到“跑一遍回归”这类任务时会传入tests接到“只跑某条用例”时会传入具体的节点路径。工具内部用subprocess调用 pytest并把最后 2000 字符的输出返回给 AI 作为判断依据。read_skill让 AI 可以主动读取规则手册。这比在系统提示词里塞大段规则更灵活因为规则可以独立更新不依赖模型版本。需要注意subprocess.run使用了timeout180这是为了限制单个用例集执行时间避免 AI 调度失控时任务一直挂在那里。FastMCP 的mcp.run()默认走 stdio 标准输入输出传输。这是目前 MCP 客户端与本地服务最常见的连接方式适合本机调试。5.2 用 MCP 客户端验证服务可用创建tools/mcp_client.pyimport asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client SERVER_PARAMS StdioServerParameters( commandpython, args[tools/mcp_server.py], cwd., ) async def main(): async with stdio_client(SERVER_PARAMS) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools_result await session.list_tools() print(可用工具) for tool in tools_result.tools: print(-, tool.name, :, tool.description) result await session.call_tool( run_pytest, arguments{target: tests/test_web.py}, ) print(\n执行结果) print(result.content[0].text) if __name__ __main__: asyncio.run(main())运行python tools/mcp_client.py预期效果是先打印出工具列表然后输出run_pytest的调用结果。如果能看到类似以下内容说明 MCP 链路已经通了可用工具 - list_test_files : 列出 tests 目录下所有测试文件清单。 - run_pytest : 运行指定测试文件、目录或用例节点的 pytest 测试。 - read_skill : 读取 skills 目录下的技能说明文件用于约束 AI 执行行为。 执行结果 PASS 1 passed in 0.42s这里真正容易踩坑的地方有两个。第一cwd建议写成绝对路径或从当前目录推导不要直接用相对路径假设命令行所在的目录。如果用相对路径MCP 客户端在不同位置启动时服务端可能找不到mcp_server.py或者 pytest 跑在错误的项目目录下。第二stdio_client的传输协议是标准输入输出所以服务端里不要随便print调试信息否则会污染协议通道。上面mcp_server.py中所有输出都通过工具返回值传递这是 MCP 服务端开发的基本纪律。6. 第三步用 Skills 约束 AI 的执行行为MCP 跑通后AI 已经有了“手”和“眼”但还没有“规矩”。这个“规矩”就是 Skills。6.1 创建 Skill 文件创建skills/web_auto.md# Web 自动化测试技能 ## 使用范围 - 只能对本地 demo.html 与测试环境域名执行浏览器操作。 - 禁止访问生产环境地址禁止操作支付、个人信息修改等高风险页面。 - 本技能只用于测试团队内部自动化回归。 ## 执行规范 1. 执行任何测试任务前先调用 read_skill 读取本文件。 2. 元素定位优先使用>import asyncio import json import requests from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client LLM_API_URL https://api.your-llm-provider.com/v1/chat/completions LLM_API_KEY sk-your-key LLM_MODEL your-model SYSTEM_PROMPT 你是 Web 自动化测试助手必须遵循以下规则 1. 执行任务前先调用 read_skill 读取 skills/web_auto.md。 2. 只允许执行 pytest 相关工具禁止尝试执行任意系统命令。 3. 如果任务不明确先列出执行计划再调用工具。 def call_llm(messages, tools): response requests.post( LLM_API_URL, headers{Authorization: fBearer {LLM_API_KEY}}, json{ model: LLM_MODEL, messages: messages, tools: tools, }, timeout60, ) response.raise_for_status() return response.json() async def main(): server_params StdioServerParameters( commandpython, args[tools/mcp_server.py], cwd., ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools_result await session.list_tools() tools [ { type: function, function: { name: t.name, description: t.description, parameters: t.inputSchema, }, } for t in tools_result.tools ] messages [ {role: system, content: SYSTEM_PROMPT}, { role: user, content: 先读取技能说明然后运行 tests 目录下的测试最后告诉我结果, }, ] response_data call_llm(messages, tools) message response_data[choices][0][message] if not message.get(tool_calls): print(模型没有返回工具调用结果, message) return for call in message[tool_calls]: fn_name call[function][name] fn_args json.loads(call[function][arguments]) if fn_name read_skill: result await session.call_tool( read_skill, argumentsfn_args, ) print(Skill 内容) print(result.content[0].text) elif fn_name run_pytest: result await session.call_tool( run_pytest, argumentsfn_args, ) print(测试结果) print(result.content[0].text) if __name__ __main__: asyncio.run(main())这是简化版 Agent 循环真实场景里还需要处理多轮调用、工具结果回传模型、失败重试等逻辑但核心链路已经完整模型发现工具 → 返回 tool_call → 客户端调用 MCP 工具 → 工具执行 pytest → 返回结果给调用方。这里有几个重点需要解释。模型向客户端返回tool_calls表示它想要调用哪些工具。这不是模型直接执行代码而是“请求调用”。真正的执行权在客户端这边。这个设计很重要因为它让每一步工具调用都在你的控制范围内你可以校验参数、记录日志、拒绝高风险操作。7.2 运行预期当你在项目根目录运行python tools/agent_runner.py流程大致是Agent 启动初始化 MCP 连接Agent 获取到read_skill、run_pytest等工具LLM 根据用户任务先返回read_skill的调用请求客户端执行read_skill把 Skill 内容打印出来LLM 继续生成第二个工具调用请求客户端执行run_pytest运行测试用例并打印结果最终 AI 返回一个结论性文本。由于不同大模型的响应节奏不同实际运行时打印的顺序可能会略有差异。但判断标准一致只要看到Skill 内容和测试结果都正常输出且测试结果里有PASS或1 passed就说明 AI 已经成功把任务拆解成工具调用并在 Skills 的约束下执行了真实测试。你可以尝试把用户任务改成先读取技能说明然后只运行 test_demo_page_interaction 这条用例看看 Agent 是否会把target参数设置为tests/test_web.py::test_demo_page_interaction。如果可以说明工具提示词和模型意图理解都已经工作。8. 常见问题与排查方法在跑通这套链路的过程中你大概率会遇到下面这些问题。我整理成一张排查表方便直接对照。问题现象可能原因排查方式解决方案pytest 报fixture page not foundconftest.py不在当前 pytest 扫描目录查看项目目录结构检查 conftest 是否和 test 文件同级或在上层目录将conftest.py放到tests/目录或按需要移动到 test 文件根目录浏览器启动失败Playwright 浏览器内核未安装运行playwright install chromium查看安装日志安装 chromium如果下载速度慢配置下载镜像地址file://地址打不开本地路径拼接错误打印DEMO_URI检查是否为file:///...绝对路径使用Path.as_uri()生成标准 URIMCP 客户端启动后没有工具列表mcp_server.py报错导致 stdio 中断先在前台运行python tools/mcp_server.py观察是否有语法错误修正服务端代码注意不要在服务端代码里随意 printMCP 服务端报FastMCP未安装依赖没有安装到当前虚拟环境执行pip list查看是否包含 fastmcp执行pip install fastmcprun_pytest返回空结果subprocess.run的 cwd 指向错误目录在工具里打印PROJECT_ROOT确认 pytest 是在项目根目录运行将cwd改为Path(__file__).resolve().parent.parentLLM 一直不返回tool_calls模型不支持 function calling或调用方式不对检查模型 API 文档确认tools参数格式换支持 function calling 的模型或检查请求体格式openai兼容接口报 401API Key 无效或鉴权头格式错误用 curl 直接调用接口测试检查Authorization头和 API Key 配置AI 执行了不在白名单内的操作Skills 没有被读取在 Agent 日志中确认是否先调用了read_skill在系统提示词中强调必须先读取 skill或在前端代码中强制注入规则截图文件生成但内容空白浏览器在截图前未等待页面稳定在截图前增加page.wait_for_load_state(networkidle)增加等待时间或在截图中加入等待逻辑这里最值得提前预防的是“AI 不按规则执行”这个问题。解决方案不是不断修改提示词而是在客户端代码里加硬校验。例如在调用run_pytest之前先检查target参数是否以tests/或demo开头如果不符合就拒绝执行。这样即使模型出了问题工具层也有最后一道防线。9. 工程化建议与安全边界链路跑通只是第一步要真正用到日常测试工作中还需要做好工程化。下面是我提炼的几个关键建议。9.1 用目录结构管理 Skills当 Skills 数量多起来后建议用统一目录结构管理每个技能一个独立目录skills/ ├── web_auto/ │ ├── SKILL.md │ └── templates/ │ └── login_case.py ├── api_regression/ │ ├── SKILL.md │ └── examples/ │ └── demo_request.jsonSKILL.md 是主说明文件配套的模板和示例放在子目录。这样 AI 读取一个技能时除了主文件还能看到可复用的代码模板减少从零生成的概率。9.2 MCP 工具要遵循最小权限原则MCP 工具不是越多越好而是越少越安全。这里的“少”指的是职责收敛一个工具只做一件事权限只覆盖必要范围。在 MCP Server 里要避免暴露通用执行接口例如不要暴露一个接受任意命令的execute_shell(command)工具不要暴露能读取任意文件路径的read_file(path)工具不要暴露能访问生产数据库的工具。如果某个操作无法确切判断是否安全宁可先不加 MCP 工具等需求明确后再补。9.3 工具参数要加白名单校验从模型传过来的参数本质上是不受信任的输入。必须在工具内部做校验。比如run_pytest的target参数可以限制只允许以tests/开头或者只允许出现在list_test_files返回的清单里ALLOWED_TARGET_PREFIXES (tests/, tests/) def validate_target(target: str) - str: if not target.startswith(ALLOWED_TARGET_PREFIXES): raise ValueError(ftarget 参数不在允许范围内: {target}) return target这种方式本质上和 Web 表单后端校验是一个思路模型在前端只是“提出请求”后端必须自己判断这个请求是否合法。9.4 执行结果要可追溯AI Agent 执行测试时一定要保留三类证据测试执行日志页面截图Agent 的工具调用记录。其中工具调用记录可以通过改造agent_runner.py实现在每次session.call_tool前后打印/保存参数和返回结果。后续如果出现误操作你可以回看 Agent 当时的判断。9.5 不要直接让 AI 修改测试代码后自动入库当前阶段让 AI 直接生成一段代码并自动提交到代码仓库仍然有风险。更稳妥的做法是AI 生成或修改测试代码开发人员 review人工确认后提交在 CI 里执行。这个流程虽然多了一步但能防止模型生成错误选择器、错误断言甚至错误业务逻辑后直接通过自动化管道进入主干。9.6 用 pytest-html 或 Allure 输出报告可以在run_pytest工具的命令里追加报告参数[ python, -m, pytest, target, -q, --tbshort, --htmlreports/report.html, ]每次 AI 执行回归后团队都能打开一份 HTML 报告查看通过率、失败原因和截图。这比 AI 用自然语言说“测试通过了”可靠得多。10. 总结Pytest Skills MCP 这套组合核心价值不是“让 AI 写脚本”而是把 AI 放进一条可约束、可执行、可验证的链路里。Pytest 提供确定性执行和断言校验MCP 负责把团队已有的测试资产开放给模型Skills 负责把领域规则外置Agent 则承担任务拆解与调度。四者结合后AI 的能力边界和测试团队的工程质量反而都有了保障。下一步你可以从这几件事开始实践把你现有的 Web 自动化用例整理进tests/目录先跑通 Pytest 层用 MCP Server 暴露一个最小工具集让 AI 能发现并调用写一份属于自己团队的 Skill 文件从“只允许访问测试环境”这类最关键的规则开始再慢慢扩展到接口自动化、性能测试、多环境切换等场景。回到标题里的“90分钟”这个时间窗口只是为了让你建立最小闭环不是终点。真正有价值的是你在这套链路里积累下来的用例资产、规则资产和工具资产。这三样资产越多AI 能帮上忙的场景就越多而你需要担心的不确定性反而越少。
返回列表