# Playwright 实战:如何从 0 到 1 写好 UI 自动化测试> 聊聊 Playwright 的核心优势、常见坑点,以及如何用 AI 工具自动生成测试代码---## 前言做测试开发这么多年,写过 Selenium、Cypress、Playwright 三种主流框架的自动化测试。**Playwright 是我用过最顺手的 UI 自动化工具**,没有之一。为什么?三点:1. **跨浏览器原生支持** — 写一次跑 Chrome/Firefox/Safari,不用折腾 WebDriver2. **自动等待机制** — 告别 `time.sleep()` 的玄学调试3. **强大调试能力** — 内置 trace viewer、codegen、debugger今天这篇文章,我分享**从环境搭建到写好第一个测试**的完整路径。---## 一、环境准备(Python + pytest-playwright)```bash# 1. 创建虚拟环境python -m venv venvsource venv/bin/activate # Windows 用 venv\Scripts\activate# 2. 安装依赖pip install pytest pytest-playwright# 3. 安装浏览器playwright install chromium# 如果需要其他浏览器:# playwright install firefox# playwright install webkit```安装完成后验证:```bashpython -c "from playwright.sync_api import sync_playwright; print('✅ Playwright OK')"```---## 二、第一个测试:登录页面### 2.1 项目结构```tests/├── conftest.py # pytest 全局 fixture
├── login/│ └── test_login.py # 登录测试用例└── pages/ └── login_page.py # Page Object```### 2.2 conftest.py — 全局配置```python# conftest.pyimport pytestfrom playwright.sync_api import Page, sync_playwright@pytest.fixture(scope="session")def browser(): """启动浏览器,整个测试会话复用同一个浏览器实例""" with sync_playwright() as p: browser = p.chromium.launch(headless=True) yield browser browser.close()@pytest.fixturedef page(browser): """每个测试用例使用独立的上下文和页面""" context = browser.new_context() page = context.new_page() yield page page.close() context.close()```**关键点**:`page` 是函数级 fixture,每个测试结束后自动清理,测试之间完全隔离。### 2.3 Page Object:登录页面```python# pages/login_page.pyfrom playwright.sync_api import Page, expectclass LoginPage: """登录页面 Page Object""" def __init__(self, page: Page): self.page = page # ============ Locators ============ @property def username_input(self): return self.page.locator('input[name="username"]') @property def password_input(self): return self.page.locator('input[name="password"]') @property def submit_btn(self): return self.page.locator('button[type="submit"]') @property def error_msg(self): return self.page.locator('.error-message') # ============ Actions ============ def goto(self, base_url: str): self.page.goto(f"{base_url}/login") def login(self, username: str, password: str): self.username_input.fill(username) self.password_input.fill(password) self.submit_btn.click() # ============ Assertions ============ def expect_title(self, title: str): expect(self.page).to_have_title(title) def expect_error(self, message: str): expect(self.error_msg).to_contain_text(message)```**为什么用 @property 定义 locators?**- 语义清晰:`page.username_input` 一眼就知道是找什么元素- 惰性查找:真正访问时才执行 `page.locator()`,避免提前报错- 统一接口:locators 和 actions 分开,代码好维护### 2.4 测试用例```python# tests/login/test_login.pyimport pytestfrom pages.login_page import LoginPageclass TestLogin: def test_login_success(self, page): """✅ 正常登录:输入正确账密,验证跳转成功""" login_page = LoginPage(page) login_page.goto("demo.example.com") login_page.login("testuser@example.com", "CorrectPass123") # 断言:登录成功后跳转到仪表盘 expect(page).to_have_url("**/dashboard") expect(page.locator(".welcome-user")).to_be_visible() def test_login_wrong_password(self, page): """❌ 密码错误:提示错误信息,不跳转""" login_page = LoginPage(page) login_page.goto("demo.example.com") login_page.login("testuser@example.com", "WrongPass456") # 断言:仍然在登录页,显示错误信息 expect(page).to_have_url("**/login") login_page.expect_error("Invalid credentials") def test_login_empty_username(self, page): """❌ 空用户名:表单校验拦截""" login_page = LoginPage(page) login_page.goto("demo.example.com") login_page.username_input.fill("any@test.com") login_page.submit_btn.click() # 断言:出现 HTML5 原生校验提示 expect(login_page.username_input).to_have_attribute("valid", "false")```### 2.5 运行测试```bash# 运行所有测试(带 HTML 报告)pytest tests/ -v --tb=short# 只运行登录测试pytest tests/login/test_login.py -v# 生成截图(在测试失败时自动截图)pytest tests/ --screenshot=only-on-failure# 生成 trace 文件(可在 UI 中回放)pytest tests/ --trace=on```---## 三、Playwright vs Selenium:核心区别| 维度 | Playwright | Selenium ||:---|:---|:---|| 启动速度 | 快 (~1-2s) | 慢 (~3-5s) || 等待机制 | **自动等待**(元素出现才操作) | 手动 sleep/显式等待 || 跨浏览器 | 原生支持三浏览器 | 依赖 WebDriver || 调试工具 | **trace viewer**(录制回放) | 截图/log || 移动端 | 原生支持 iOS/Android | 需要 Appium || 脚本书写 | JS/Python | Java/Python/C#/... |**最重要的区别:自动等待**Selenium 你需要写:```python# Selenium:手动等待,繁琐且容易过时from selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as ECelement = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "submit")))```Playwright 只需:```python# Playwright:自动等待,写法和正常操作一样page.click("#submit") # playwright 会等元素可点击再点```---## 四、常见坑点与解决方案### 坑点 1:元素找不到 — 动态加载的内容**问题**:页面有 loading spinner,数据是异步加载的,测试跑的时候元素还没出现。**解决**:用 `wait_for_selector` 或 Playwright 的自动等待```python# ✅ 推荐:显式等待特定元素出现page.wait_for_selector(".data-table tr", timeout=10000)# ✅ 更好:结合断言,用 expectexpect(page.locator(".data-table tr")).to_have_count(5)# ❌ 错误:不要用 sleepimport timetime.sleep(3) # 浪费时间,且不稳定```### 坑点 2:多标签页(Tab)切换**问题**:点击链接弹出新窗口,不知道怎么切过去。**解决**:`page.context.wait_for_event('page')````python# 触发打开新标签的动作async with page.context.expect_page() as new_page_info: await page.click('a[target="_blank"]') # 点击新窗口链接new_page = await new_page_info.valueawait new_page.wait_for_load_state()# 在新页面操作await new_page.fill("#search", "Playwright")```### 坑点 3:iframe 里的元素**问题**:元素在 `` 里,直接 `page.locator()` 找不到。**解决**:用 `frame_locator()````python# ❌ 找不到page.locator("#iframe input[name='username']")# ✅ 正确:先进入 frameframe = page.frame_locator("#login-iframe")frame.locator("input[name='username']").fill("user")```### 坑点 4:HEADLESS 模式下文件上传**问题**:文件上传 input 是隐藏的,Playwright 不好操作。**解决**:用 `set_input_files` 直接赋值```python# ✅ 推荐:直接设置文件路径,不用找隐藏的 inputpage.locator('input[type="file"]').set_input_files("/path/to/test.pdf")# 清除文件page.locator('input[type="file"]').set_input_files([])```---## 五、如何用 AI 自动生成测试代码手工写测试很费时间,特别是重复的 CRUD 流程测试。我做了一个 **Playwright Test Generator 工具**,可以从以下三种方式自动生成测试代码:```bash# 方式1:从 URL 分析页面结构,生成测试代码playwright-gen url https://example.com --lang python --pom# 方式2:从用户故事生成测试playwright-gen story "As a user I can login with email" --lang python# 方式3:从自然语言描述生成测试playwright-gen desc "打开登录页,输入账密,点击提交,验证跳转" --lang python```**自动生成的代码示例**(从 URL 分析):```python"""Generated Playwright test - Example Domain."""import pytestfrom playwright.sync_api import Page, expectclass TestExampleLogin: """Test class for Example Domain login page""" @pytest.fixture(autouse=True) def setup(self, page: Page): self.page = page def test_login(self): """Generated login test.""" self.page.goto("example.com") self.page.click("#submit") # Submit button expect(self.page).to_have_url(r"**/example.com")```**配合 Page Object 使用,效果更好**:```python# 自动生成的 POMclass LoginPage: def __init__(self, page: Page): self.page = page @property def username_input(self): return self.page.locator("#username") @property def password_input(self): return self.page.locator("#password") @property def submit_btn(self): return self.page.locator("button[type='submit']") def login(self, username: str, password: str): self.username_input.fill(username) self.password_input.fill(password) self.submit_btn.click()```---## 六、总结今天分享了:| 知识点 | 重点 ||:---|:---|| 环境搭建 | pytest-playwright + conftest fixture || Page Object | locators 用 @property,actions 和 assertions 分离 || 第一个测试 | 登录成功/失败/空输入 三个场景 || 自动等待 | Playwright 的核心优势,少用 sleep || AI 生成 | 用工具自动生成 POM 和测试脚手架 |**下期预告**:《Playwright + AI:Page Object Model 进阶设计,用 AI 自动生成可维护的测试代码》,我会详细讲解:1. 如何设计一套适合团队的 POM 结构2. 数据驱动测试怎么做3. 用 AI 工具自动生成 Page Object 并接入 CI/CD---> 关注我,我会持续分享测试开发实战经验 🚀>> 相关项目:[playwright-test-generator](github.com/) — AI 驱动的 Playwright 测试代码生成工具