端到端测试一直有个矛盾——写得太死,跟不上产品变化;写得太活,又没法稳定复现。AI Agent 进入测试栈后,这个矛盾有了新的解法,但也带来了新的混淆:Agent 该替代传统测试,还是叠加在上面?一支团队跑了 200 多条 agentic E2E workflow,用 Playwright MCP、Playwright CLI 和 Agent 自动生成的 Playwright 测试脚本,在非生产数据的工作空间里反复验证,得出的结论很明确——Agent 是探索层,不是确定性层。
传统 E2E 测试的确定性瓶颈
传统 Playwright 测试的核心思路是:定位元素 → 操作 → 断言结果。每一步都写死在代码里:
# 传统确定性测试——每一步都硬编码
from playwright.sync_api import sync_playwright
def test_login_flow():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://app.example.com/login")
page.fill("#email", "user@test.com")
page.fill("#password", "secret123")
page.click("button[type='submit']")
assert page.url == "https://app.example.com/dashboard"
browser.close()
这种测试的优点是稳定、可复现、CI 里跑一千次结果一致。缺点也明显:页面改一个 selector,几十条测试全挂;新功能上线,测试覆盖永远滞后;只能测"已知路径",测不了"用户可能乱点出来的意外路径"。
Agent 进入测试栈的三种形态
实验中出现了三种 Agent 参与 E2E 测试的方式,各有适用边界:
形态一:Playwright MCP——Agent 实时操控浏览器
Playwright MCP 把浏览器操作暴露为 Model Context Protocol 的工具调用。Agent 不写脚本,而是实时决策下一步操作:
// MCP 工具调用示例——Agent 通过工具协议直接操控浏览器
{
"tool": "playwright_navigate",
"arguments": { "url": "https://app.example.com/login" }
}
// Agent 观察页面后自主决定下一步
{
"tool": "playwright_click",
"arguments": { "selector": "button[data-testid='submit']" }
}
这种方式最灵活,Agent 可以根据页面实际状态动态调整路径。但每次运行结果可能不同——Agent 这次走了登录流程,下次可能先逛了一圈设置页。适合探索性测试,不适合 CI 门禁。
形态二:Playwright CLI——Agent 调用命令行工具
Agent 通过 shell 命令驱动 Playwright CLI,适合需要批量操作或与脚本组合的场景:
# Agent 生成并执行的 CLI 命令序列
npx playwright codegen --target=python https://app.example.com
# Agent 也可以直接运行已有测试并分析结果
npx playwright test --reporter=json tests/smoke/ 2>&1 | \
jq '.suites[].tests[] | select(.status=="failed") | .name'
CLI 方式让 Agent 可以混合使用"生成脚本"和"执行脚本"两种能力,但本质上还是非确定性的——Agent 每次可能生成不同的命令序列。
形态三:Agent 生成 Playwright 测试代码
Agent 不直接跑测试,而是生成确定性测试脚本,再由传统流程执行:
# Agent 自动生成的 Playwright 测试——写完后人审、CI 跑
import re
from playwright.sync_api import Page, expect
def test_checkout_with_discount(page: Page):
"""Agent 生成:验证折扣码在结算流程中正确生效"""
page.goto("/cart")
# Agent 学会了用 data-testid 而非脆弱的 CSS selector
page.click("[data-testid='proceed-to-checkout']")
page.fill("[data-testid='discount-code']", "SAVE20")
page.click("[data-testid='apply-discount']")
# 断言折扣金额出现且数值合理
discount_text = page.locator("[data-testid='discount-amount']").text_content()
discount_value = float(re.search(r"\d+\.\d{2}", discount_text).group())
assert discount_value > 0, "折扣金额应为正数"
subtotal = float(page.locator("[data-testid='subtotal']").text_content().split("$")[1])
assert discount_value <= subtotal * 0.3, "折扣不应超过小计的30%"
这是三种形态中唯一适合进 CI 的——生成的代码是确定性的,人审核后锁定版本,后续运行结果稳定可复现。
实践:搭建一个两层测试栈
把探索层和确定性层分开,是实验最重要的结论。下面是一个可以直接改造使用的项目结构:
# 项目目录结构
tests/
├── deterministic/ # 传统 + Agent 生成的确定性测试,进 CI
│ ├── login.spec.py
│ ├── checkout.spec.py # Agent 生成,人审核后提交
│ └── smoke/
├── exploratory/ # Agent 实时探索,不进 CI,本地或定时跑
│ ├── agent_mcp_configs/ # MCP 工具配置
│ └── exploration_prompts/
└── shared/
└── test_data.py # 非生产测试数据工厂
确定性测试的 CI 配置:
# .github/workflows/e2e-deterministic.yml
name: E2E Deterministic Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install playwright pytest
- run: playwright install chromium
- run: pytest tests/deterministic/ --timeout=60s
env:
TEST_ENV: staging # 只用非生产数据
探索性测试的定时任务(不阻塞 CI):
# .github/workflows/e2e-exploratory.yml
name: E2E Exploratory Agent Scan
on:
schedule:
- cron: "0 6 * * 1-5" # 工作日早上跑一次
jobs:
explore:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run agentic exploration
run: |
npx @anthropic-ai/claude-code \
--prompt "用 Playwright MCP 工具探索 staging 环境的完整用户流程,记录所有异常行为" \
--allowedTools "playwright_navigate,playwright_click,playwright_fill,playwright_screenshot" \
--maxTurns 50
env:
MCP_ENDPOINT: ${{ secrets.STAGING_MCP_URL }}
- name: Parse agent findings
run: python scripts/parse_exploration_report.py
- name: Create issues for bugs found
uses: actions/create-issue@v1
with:
title-prefix: "[Exploratory] "
关键设计决策:探索层发现问题后,不是直接修代码,而是把 bug 转成确定性测试再进 CI。这保证了修复验证是可复现的。
测试数据:非生产环境是底线
实验明确要求所有 agentic 测试都在非生产数据环境运行。Agent 的行为不可完全预测,让它操作生产数据库是高风险行为。实践中可以这样隔离:
# shared/test_data.py——非生产数据工厂
import faker
fake = faker.Faker()
def create_test_user(env="staging"):
"""只在 staging 创建测试用户,绝不碰生产"""
return {
"email": f"e2e_test_{fake.uuid4()}@test.example.com",
"password": "TestPass!2024",
"env": env, # 确认环境标记
}
# Agent 生成的测试必须引用这个工厂,不能硬编码真实用户
该把 Agent 放在哪一层?一个决策清单
根据 200 多条 workflow 的实验结果,可以这样决策:
| 场景 | 用什么 | 进 CI? |
|---|---|---|
| 核心业务流程回归 | 传统 Playwright 或 Agent 生成的脚本 | ✅ 是 |
| 新功能首次覆盖 | Agent 生成脚本 → 人审核 → 提交 | ✅ 审核后是 |
| 未知路径探索、边缘 case | Playwright MCP 实时探索 | ❌ 否 |
| 大范围冒烟扫描 | Playwright CLI + Agent 批量执行 | ❌ 否,结果转 issue |
| 性能/并发压测 | 传统工具,Agent 不适合 | ❌ Agent 延迟不可控 |
不要做的事:把 MCP 实时探索放进 CI pipeline 当门禁。Agent 这次跑通了,下次可能选了不同路径,CI 会变成随机通过/失败,团队很快就会忽略测试结果。
应该做的事:让 Agent 每天定时探索,发现的 bug 转成确定性测试,再进 CI。这样 Agent 的不确定性变成了发现能力,确定性测试保证了验证的可靠。
风险和边界
实验也暴露了几个实际问题:
- Agent 幻觉定位器:Agent 有时会生成页面上不存在的 selector,MCP 模式下会反复尝试无效操作。解决方法是给 Agent 提供
data-testid映射表作为上下文。 - 成本不可忽视:200 条 workflow 的 token 消耗不小。探索层应该限制
maxTurns,避免 Agent 无限循环。 - 结果归因困难:Agent 探索发现了一个 bug,但复现步骤可能不清晰。需要让 Agent 在探索时同步截图和 DOM 快照,方便后续写确定性测试时参考。
Agentic E2E 测试不是推翻旧体系,而是在确定性测试之上加一层"会思考的探索"。两层各司其职,确定性层守门,探索层找盲区——这才是 Agent 在测试栈里的正确位置。