Webapp Testing 完整指南
本文档包含使用 Playwright 测试 Web 应用的完整技术细节。如果您是初次接触,建议先阅读 📋 概览。
目录
核心概念
Playwright 简介
Playwright 是微软开发的现代浏览器自动化工具:
- ✅ 跨浏览器:Chromium、Firefox、WebKit
- ✅ 速度快:比 Selenium 快 2-3 倍
- ✅ 自动等待:智能等待元素出现
- ✅ 功能丰富:截图、录屏、网络监控
决策树
用户任务
│
▼
静态 HTML?
├─ 是 → 直接读取 HTML → 编写脚本 → file:// URL
└─ 否 → 动态应用
│
▼
服务器已运行?
├─ 否 → with_server.py 启动
└─ 是 → 侦察-行动模式
辅助工具:with_server.py
功能
自动管理服务器生命周期:启动 → 等待就绪 → 运行测试 → 清理
基本用法
# 单服务器
python scripts/with_server.py \
--server "npm run dev" \
--port 5173 \
-- python test_app.py
# 多服务器(前后端)
python scripts/with_server.py \
--server "cd backend && python app.py" --port 3000 \
--server "cd frontend && npm run dev" --port 5173 \
-- python test_fullstack.py
测试脚本
# test_app.py - 无需管理服务器
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# 服务器已就绪
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle')
page.click('button:has-text("登录")')
browser.close()
侦察-行动模式
完整流程
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# 1. 导航
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle') # 关键!
# 2. 侦察:截图
page.screenshot(path='/tmp/inspect.png', full_page=True)
# 3. 侦察:发现元素
buttons = page.locator('button').all()
for btn in buttons:
print(f"发现按钮: {btn.text_content()}")
# 4. 行动:使用发现的选择器
page.click('button:has-text("提交")')
page.wait_for_selector('text=提交成功')
browser.close()
选择器速查
推荐优先级
# 1. 角色选择器(最佳)
page.click('role=button[name="提交"]')
page.fill('role=textbox[name="用户名"]', 'admin')
# 2. 文本选择器
page.click('text=登录')
page.click('button:has-text("确认")')
# 3. ID/CSS 选择器
page.click('#submit-button')
page.fill('input[name="email"]', 'test@example.com')
# 4. XPath(最后选择)
page.click('//button[contains(text(), "提交")]')
组合选择器
# CSS >> 文本
page.click('form >> text=登录')
# 多级选择
page.click('main >> section >> button:has-text("确认")')
等待策略
网络空闲(动态应用必需)
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle') # 等待 JS 执行
元素等待
# 等待出现
page.wait_for_selector('#success-message', timeout=5000)
# 等待消失
page.wait_for_selector('#loading', state='hidden')
# 等待可点击
page.wait_for_selector('button:not([disabled])')
自定义条件
# JavaScript 条件
page.wait_for_function('() => document.querySelector("#data").innerText !== "Loading..."')
# URL 等待
page.wait_for_url('**/dashboard')
截图和录屏
截图
# 全页截图
page.screenshot(path='screenshot.png', full_page=True)
# 元素截图
page.locator('#chart').screenshot(path='chart.png')
# 指定质量
page.screenshot(path='screenshot.jpg', type='jpeg', quality=80)
录屏
context = browser.new_context(
record_video_dir='./videos',
record_video_size={'width': 1280, 'height': 720}
)
page = context.new_page()
# 执行测试...
context.close() # 视频自动保存
控制台日志捕获
console_messages = []
page.on('console', lambda msg: console_messages.append({
'type': msg.type,
'text': msg.text
}))
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle')
# 执行操作...
# 分析日志
errors = [m for m in console_messages if m['type'] == 'error']
if errors:
print(f"发现 {len(errors)} 个错误")
for err in errors:
print(f" - {err['text']}")
网络监控
监听请求/响应
# 监听请求
page.on('request', lambda req: print(f"→ {req.method} {req.url}"))
# 监听响应
page.on('response', lambda res: print(f"← {res.status} {res.url}"))
# 过滤 API 请求
def log_api(request):
if '/api/' in request.url:
print(f"API: {request.method} {request.url}")
page.on('request', log_api)
拦截和修改
# 拦截特定请求
page.route('**/api/data', lambda route: route.fulfill(
status=200,
body='{"mocked": true}'
))
# 阻止资源加载
page.route('**/*.{png,jpg,jpeg}', lambda route: route.abort())
常见陷阱
❌ 陷阱 1:忘记等待
# 错误
page.goto('http://localhost:5173')
page.click('button') # 失败:元素不存在
# 正确
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle')
page.click('button')
❌ 陷阱 2:脆弱选择器
# 错误:依赖 DOM 结构
page.click('div > div > button:nth-child(3)')
# 正确:语义化选择器
page.click('button:has-text("提交")')
page.click('role=button[name="提交"]')
❌ 陷阱 3:异步操作
# 错误:立即检查结果
page.click('button#load-data')
data = page.locator('#data').text_content()
# 正确:等待加载完成
page.click('button#load-data')
page.wait_for_selector('#data:not(:has-text("Loading..."))')
data = page.locator('#data').text_content()
调试技巧
交互式调试
browser = p.chromium.launch(headless=False) # 可视模式
page = browser.new_page()
page.goto('http://localhost:5173')
page.pause() # 暂停,打开调试器
慢动作模式
browser = p.chromium.launch(
headless=False,
slow_mo=1000 # 每步延迟 1 秒
)
详细日志
DEBUG=pw:api python test.py
完整示例
示例 1:登录流程测试
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# 1. 访问登录页
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle')
# 2. 填写表单
page.fill('input[name="username"]', 'admin')
page.fill('input[name="password"]', 'password123')
# 3. 提交
page.click('button:has-text("登录")')
# 4. 验证成功
page.wait_for_selector('text=欢迎')
assert '欢迎' in page.content()
print("✅ 登录测试通过")
browser.close()
示例 2:表单验证测试
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('http://localhost:5173/register')
page.wait_for_load_state('networkidle')
# 测试空表单提交
page.click('button[type="submit"]')
error = page.locator('.error-message').text_content()
assert '请填写必填项' in error
# 测试邮箱格式
page.fill('input[name="email"]', 'invalid-email')
page.click('button[type="submit"]')
error = page.locator('.error-message').text_content()
assert '邮箱格式不正确' in error
print("✅ 表单验证测试通过")
browser.close()
示例 3:购物流程测试
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# 登录
page.goto('http://localhost:5173/login')
page.wait_for_load_state('networkidle')
page.fill('input[name="username"]', 'customer')
page.fill('input[name="password"]', 'password')
page.click('button:has-text("登录")')
# 浏览商品
page.wait_for_url('**/dashboard')
page.click('a:has-text("商品列表")')
# 添加到购物车
page.click('button.add-to-cart:nth-of-type(1)')
page.wait_for_selector('text=已添加到购物车')
# 结算
page.click('a:has-text("购物车")')
page.click('button:has-text("结算")')
# 验证订单
page.wait_for_selector('text=订单创建成功')
order_number = page.locator('#order-number').text_content()
print(f"✅ 订单创建成功: {order_number}")
browser.close()
参考资源
官方文档
- Playwright Python: https://playwright.dev/python/
- API 参考: https://playwright.dev/python/docs/api/class-playwright
- 选择器: https://playwright.dev/python/docs/selectors
示例文件
原始技能包含以下示例(位于 examples/ 目录):
element_discovery.py- 元素发现示例static_html_automation.py- 静态 HTML 测试console_logging.py- 控制台日志捕获
辅助脚本
scripts/with_server.py- 服务器生命周期管理
提示
始终先运行 --help 了解脚本用法,避免阅读源码污染上下文。