Playwright 自动化测试教程
1. Playwright 简介
Playwright 是一个现代化的端到端测试框架,支持 Chromium、Firefox 和 WebKit 浏览器。它提供跨平台的测试能力,支持 Windows、Linux 和 macOS 系统。
主要特点:
- 支持多种浏览器
- 跨平台测试
- 支持无头(Headless)和有头模式:
- 无头模式:浏览器在后台运行,不显示UI界面,适合自动化测试和CI/CD环境
- 有头模式:浏览器显示完整UI界面,适合调试和开发阶段观察测试过程
- 移动设备模拟
2. 安装与设置
Node.js 环境
npm init playwright@latest
Python 环境
pip install playwright
playwright install
3. 基础用法
启动浏览器
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await browser.close();
})();
Python 同步模式
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://playwright.dev")
print(page.title())
browser.close()
4. 元素定位与交互
基本定位方法
// 通过文本定位
await page.getByText('Submit').click();
// 通过角色定位
await page.getByRole('button', { name: 'Sign in' }).click();
// 通过标签定位
await page.getByLabel('User Name').fill('John');
常见交互操作
// 点击
await page.locator('#submit').click();
// 输入文本
await page.locator('#search').fill('query');
// 拖放
await source.dragTo(target);
5. 断言与验证
基本断言
// 验证标题
await expect(page).toHaveTitle(/Playwright/);
// 验证元素可见
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
// 验证元素包含文本
await expect(page.locator('.status')).toContainText('Success');
6. 高级功能
截图
// 全屏截图
await page.screenshot({ path: 'screenshot.png' });
// 元素截图
await page.locator('.header').screenshot({ path: 'header.png' });
HTTP 认证
const context = await browser.newContext({
httpCredentials: {
username: 'bill',
password: 'pa55w0rd'
}
});
视觉回归测试
test('example test', async ({ page }) => {
await page.goto('https://playwright.dev');
await expect(page).toHaveScreenshot();
});
7. 测试框架集成
Playwright Test
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
await page.goto('https://playwright.dev/');
const name = await page.innerText('.navbar__title');
expect(name).toBe('Playwright');
});
Python pytest
import re
from playwright.sync_api import Page, expect
def test_has_title(page: Page):
page.goto("https://playwright.dev/")
expect(page).to_have_title(re.compile("Playwright"))
设置用户数据目录,保存用户状态
下次自动打开浏览器的时候,能保持用户数据和登陆状态
with sync_playwright() as p:
user_data_dir = "user_data" # 用户数据目录,用于保存cookie等
context = p.chromium.launch_persistent_context(
user_data_dir,
headless=False
)
page = context.new_page()
page.goto("https://www.xiaohongshu.com/explore")
8. 最佳实践
- 使用明确的定位器而非XPath
- 优先使用getByRole、getByText等语义化定位方法
- 合理使用waitFor系列方法
- 保持测试独立性和可重复性
- 使用Page Object模式组织测试代码
9. 常见问题
元素定位失败
- 检查元素是否在iframe中
- 确认元素已加载完成
- 使用page.pause()调试
测试不稳定
- 增加适当的等待
- 使用可靠的定位策略
- 避免依赖外部服务
10. 资源
11. Python Playwright 综合案例
下面是一个综合案例,展示了如何使用 Python 和 Playwright 进行网页自动化操作,包括网页截图、数据提取和表单交互等功能。
11.1 电商网站商品信息爬取
这个案例演示了如何使用 Playwright 爬取电商网站的商品信息,包括标题、价格和评分等数据。
import asyncio
from playwright.async_api import async_playwright
import pandas as pd
import time
async def scrape_products():
"""爬取电商网站商品信息"""
# 存储结果的列表
products = []
async with async_playwright() as p:
# 启动浏览器(使用有头模式便于观察)
browser = await p.chromium.launch(headless=False)
# 创建新的上下文和页面
context = await browser.new_context(
viewport={"width": 1280, "height": 800},
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36"
)
page = await context.new_page()
# 导航到目标网站
await page.goto("https://demo.opencart.com/")
# 等待页面加载完成
await page.wait_for_load_state("networkidle")
# 截图首页
await page.screenshot(path="homepage.png", full_page=True)
# 点击进入笔记本电脑分类
await page.click("text=Laptops & Notebooks")
await page.click("text=Show All Laptops & Notebooks")
# 等待产品列表加载
await page.wait_for_selector(".product-layout")
# 提取产品信息
product_cards = await page.query_selector_all(".product-layout")
for card in product_cards:
try:
# 提取产品名称
name_element = await card.query_selector("h4 a")
name = await name_element.inner_text() if name_element else "未知产品"
# 提取产品价格
price_element = await card.query_selector(".price")
price = await price_element.inner_text() if price_element else "价格未知"
# 提取产品评分
rating_element = await card.query_selector(".rating")
rating_stars = await rating_element.query_selector_all("span.fa-stack") if rating_element else []
rating = len([star for star in rating_stars if await star.get_attribute("class") and "fa-star" in await star.get_attribute("class")])
# 获取产品链接
link_element = await card.query_selector("h4 a")
link = await link_element.get_attribute("href") if link_element else ""
# 添加到产品列表
products.append({
"name": name,
"price": price.split("\n")[0] if "\n" in price else price, # 处理可能的特价显示
"rating": rating,
"link": link
})
except Exception as e:
print(f"提取产品信息时出错: {e}")
# 将结果保存为CSV
df = pd.DataFrame(products)
df.to_csv("products.csv", index=False, encoding="utf-8")
# 关闭浏览器
await browser.close()
return products
# 运行爬虫
if __name__ == "__main__":
products = asyncio.run(scrape_products())
print(f"共爬取了 {len(products)} 个产品")
for product in products:
print(f"产品: {product['name']}, 价格: {product['price']}, 评分: {product['rating']}星")
11.2 自动化表单填写与提交
这个案例展示了如何使用 Playwright 自动填写表单并提交,包括处理不同类型的表单元素。
from playwright.sync_api import sync_playwright
import time
def automate_form_submission():
"""自动化表单填写与提交"""
with sync_playwright() as p:
# 启动浏览器
browser = p.chromium.launch(headless=False)
# 创建新的上下文和页面
page = browser.new_page()
# 导航到表单页面
page.goto("https://demoqa.com/automation-practice-form")
# 填写个人信息
page.fill("#firstName", "张")
page.fill("#lastName", "三")
page.fill("#userEmail", "zhangsan@example.com")
# 选择性别单选按钮
page.click("label[for='gender-radio-1']")
# 填写手机号码
page.fill("#userNumber", "1381234567")
# 选择出生日期
page.click("#dateOfBirthInput")
page.select_option(".react-datepicker__month-select", "3") # 4月
page.select_option(".react-datepicker__year-select", "1990")
page.click(".react-datepicker__day--010") # 选择10号
# 填写学科
page.click("#subjectsContainer")
page.type("#subjectsInput", "Math")
page.press("#subjectsInput", "Enter")
page.type("#subjectsInput", "Computer")
page.press("#subjectsInput", "Enter")
# 选择爱好复选框
page.click("label[for='hobbies-checkbox-1']") # 运动
page.click("label[for='hobbies-checkbox-3']") # 音乐
# 上传图片
# 注意:需要提供一个实际存在的文件路径
# page.set_input_files("#uploadPicture", "path/to/picture.jpg")
# 填写当前地址
page.fill("#currentAddress", "北京市海淀区中关村大街1号")
# 选择州和城市
page.click("#state")
page.click("text=NCR")
page.click("#city")
page.click("text=Delhi")
# 提交表单
page.click("#submit")
# 等待提交后的确认对话框
page.wait_for_selector(".modal-content")
# 截图确认结果
page.screenshot(path="form_submission_result.png")
# 验证提交成功
success_text = page.inner_text(".modal-content")
print("表单提交结果:", success_text)
# 关闭浏览器
browser.close()
if __name__ == "__main__":
automate_form_submission()
11.3 多浏览器并行测试
这个案例展示了如何使用 Playwright 在多个浏览器上并行运行测试,比较不同浏览器的性能和兼容性。
import asyncio
from playwright.async_api import async_playwright
import time
async def run_performance_test():
"""在多个浏览器上并行测试网站性能"""
async with async_playwright() as p:
# 定义要测试的浏览器
browsers = [
{"name": "Chromium", "browser_type": p.chromium},
{"name": "Firefox", "browser_type": p.firefox},
{"name": "WebKit", "browser_type": p.webkit}
]
# 定义测试网站
test_sites = [
{"name": "Google", "url": "https://www.google.com"},
{"name": "GitHub", "url": "https://github.com"},
{"name": "Stack Overflow", "url": "https://stackoverflow.com"}
]
results = []
# 创建测试任务
tasks = []
for browser in browsers:
for site in test_sites:
tasks.append(test_site_performance(browser, site))
# 并行运行所有测试
results = await asyncio.gather(*tasks)
# 打印结果
print("\n性能测试结果:")
print("-" * 80)
print(f"{'浏览器':<10} {'网站':<15} {'加载时间(ms)':<15} {'内存使用(MB)':<15}")
print("-" * 80)
for result in results:
print(f"{result['browser']:<10} {result['site']:<15} {result['load_time']:<15.2f} {result['memory']:<15.2f}")
return results
async def test_site_performance(browser_info, site_info):
"""测试特定浏览器和网站的性能"""
browser_name = browser_info["name"]
site_name = site_info["name"]
site_url = site_info["url"]
# 启动浏览器
browser = await browser_info["browser_type"].launch()
try:
# 创建新的上下文和页面
context = await browser.new_context()
page = await context.new_page()
# 测量页面加载时间
start_time = time.time()
# 导航到网站
response = await page.goto(site_url)
# 等待页面完全加载
await page.wait_for_load_state("networkidle")
# 计算加载时间(毫秒)
load_time = (time.time() - start_time) * 1000
# 获取内存使用情况
client = await page.context.new_cdp_session(page)
performance_stats = await client.send("Performance.getMetrics")
memory_usage = next((metric["value"] for metric in performance_stats["metrics"]
if metric["name"] == "JSHeapUsedSize"), 0) / (1024 * 1024) # 转换为MB
# 截图
await page.screenshot(path=f"{browser_name}_{site_name}.png")
return {
"browser": browser_name,
"site": site_name,
"load_time": load_time,
"memory": memory_usage
}
finally:
# 关闭浏览器
await browser.close()
# 运行测试
if __name__ == "__main__":
asyncio.run(run_performance_test())
11.4 网页监控与自动化报告
这个案例展示了如何使用 Playwright 定期监控网站状态,并生成报告。
import asyncio
from playwright.async_api import async_playwright
import datetime
import pandas as pd
import matplotlib.pyplot as plt
import os
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
# 监控配置
MONITORING_INTERVAL = 3600 # 每小时检查一次
WEBSITES = [
{"name": "Google", "url": "https://www.google.com"},
{"name": "Baidu", "url": "https://www.baidu.com"},
{"name": "GitHub", "url": "https://github.com"}
]
REPORT_DIR = "monitoring_reports"
# 确保报告目录存在
os.makedirs(REPORT_DIR, exist_ok=True)
async def monitor_website(website):
"""监控单个网站的状态和性能"""
name = website["name"]
url = website["url"]
timestamp = datetime.datetime.now()
async with async_playwright() as p:
try:
# 启动浏览器
browser = await p.chromium.launch()
context = await browser.new_context()
page = await context.new_page()
# 记录开始时间
start_time = time.time()
# 导航到网站
response = await page.goto(url, timeout=30000)
status = response.status if response else 0
# 等待页面加载完成
await page.wait_for_load_state("networkidle")
# 计算加载时间
load_time = (time.time() - start_time) * 1000 # 毫秒
# 截图
screenshot_path = f"{REPORT_DIR}/{name}_{timestamp.strftime('%Y%m%d_%H%M%S')}.png"
await page.screenshot(path=screenshot_path, full_page=True)
# 关闭浏览器
await browser.close()
return {
"name": name,
"url": url,
"timestamp": timestamp,
"status": status,
"load_time": load_time,
"screenshot_path": screenshot_path,
"success": True,
"error": None
}
except Exception as e:
# 处理错误
return {
"name": name,
"url": url,
"timestamp": timestamp,
"status": 0,
"load_time": 0,
"screenshot_path": None,
"success": False,
"error": str(e)
}
async def run_monitoring_cycle():
"""运行一次完整的监控周期"""
print(f"开始监控周期: {datetime.datetime.now()}")
# 并行监控所有网站
tasks = [monitor_website(website) for website in WEBSITES]
results = await asyncio.gather(*tasks)
# 保存结果到CSV
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
df = pd.DataFrame(results)
csv_path = f"{REPORT_DIR}/monitoring_{timestamp}.csv"
df.to_csv(csv_path, index=False)
# 生成报告
generate_report(results, timestamp)
print(f"监控周期完成: {datetime.datetime.now()}")
return results