1. 环境搭建:从零到一,打造你的自动化测试工作站

想玩转Python和Playwright自动化测试,第一步就是把“家伙事儿”备齐。这就像你要组装一台电脑,得先把CPU、主板、内存条买回来一样。环境搭建听起来有点技术门槛,但别怕,跟着我的步骤走,保证你十分钟内就能搞定,而且我还会告诉你一些官方文档里没提的“坑”,让你少走弯路。

首先,你得确保你的电脑上已经安装了Python。Playwright对Python版本有要求,必须是3.8或更高。怎么检查?很简单,打开你的命令行(Windows上是CMD或PowerShell,Mac或Linux上是终端),输入 python --version 或者 python3 --version,回车后看看版本号是不是3.8以上。如果还没安装Python,直接去Python官网下载最新的稳定版安装包,安装时记得勾选“Add Python to PATH”这个选项,这能省去后面配置环境变量的大麻烦。

Python就位后,安装Playwright库就是一句话的事。我强烈建议你不要用系统自带的命令行,而是以管理员身份运行,这样可以避免一些权限问题。打开你的命令行工具,输入这条命令:pip install playwright。这里的pip是Python的包管理工具,它会自动从网络上下载Playwright及其所有依赖。安装过程可能会花一两分钟,取决于你的网速。安装完成后,你可以用 pip show playwright 来确认一下版本信息,确保安装成功了。

光有Playwright库还不够,我们还需要它能控制的浏览器。Playwright的强大之处在于它原生支持Chromium(Chrome的开源核心)、Firefox和WebKit(Safari的渲染引擎)。你不需要提前在电脑上安装这些浏览器,Playwright会自己下载一套专门用于自动化测试的、干净的浏览器版本。执行命令 playwright install,它会一次性把三大浏览器都下载下来。如果你只想测试Chrome,也可以只安装Chromium:playwright install chromium。这些浏览器会被安装到一个固定的用户目录下,比如Windows通常在 C:\Users\<你的用户名>\AppData\Local\ms-playwright,这样不会和你日常使用的浏览器冲突。

这里有个小技巧,如果你身处网络环境不太理想的地方,下载浏览器可能会非常慢甚至失败。别急,Playwright支持离线安装。你可以找一台网络好的机器,先执行playwright install下载好,然后把整个ms-playwright文件夹拷贝到你的电脑对应目录下,一样能用。环境搭好,我们写个最简单的“Hello World”脚本来验货。创建一个新的Python文件,比如叫test_demo.py,输入下面的代码:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    # 启动一个Chromium浏览器,headless=False表示我们会看到浏览器窗口
    browser = p.chromium.launch(headless=False)
    # 打开一个新页面
    page = browser.new_page()
    # 让页面跳转到百度
    page.goto("https://www.baidu.com")
    # 打印当前页面的标题
    print(f"页面标题是:{page.title()}")
    # 等待5秒,让我们能看到浏览器
    page.wait_for_timeout(5000)
    # 关闭浏览器
    browser.close()

保存后,在命令行里运行 python test_demo.py。如果一切顺利,你会看到一个Chrome浏览器窗口自动打开,访问百度,然后在命令行里打印出“百度一下,你就知道”,几秒后浏览器自动关闭。恭喜你,你的第一个Playwright自动化脚本已经成功运行了!这个过程是不是比想象中简单?关键在于每一步都踩准了,接下来我们就可以深入更多有趣的功能了。

1.1 同步与异步:选择适合你的编程模式

在写Playwright脚本时,你会遇到一个关键选择:用同步API还是异步API?这可能是新手第一个容易懵的地方。我用大白话解释一下:同步就是“一步一步来”,代码从上到下执行,上一步没做完就等着,做完了才做下一步;异步则是“多件事一起推进”,发起一个操作后不用干等着,可以先去处理别的代码,等那个操作完成了再回来处理结果。

我们上面例子用的 sync_playwright 就是同步模式,代码逻辑直来直去,非常适合初学者理解和编写。它的写法就像我们平时写Python脚本一样,顺序执行,容易调试。但它的缺点是,如果页面加载很慢,或者某个操作(如下载文件)耗时很长,你的整个脚本就会“卡”在那里等待,效率不高。

而异步模式(使用 async_playwright)则像是一个高效的指挥家。它允许你在等待网络请求或页面渲染的同时,执行其他任务。这对于需要同时操作多个页面(标签页)、或者编写高性能爬虫和复杂测试流程的场景非常有用。它的代码结构会用到 async 和 await 关键字。我们来看一个异步模式的简单例子:

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)
        page = await browser.new_page()
        await page.goto("https://www.example.com")
        print(await page.title())
        await browser.close()

# 运行异步函数
asyncio.run(main())

看到区别了吗?几乎每个可能耗时的操作前面都加上了 await,意思是“我发起这个操作,然后你可以先去忙别的,等有结果了再通知我”。对于新手,我个人的建议是:先从同步模式开始。把元素定位、基本操作玩熟了,再去挑战异步模式。因为异步编程需要更小心地处理任务调度和异常,一开始容易出错。99%的日常Web自动化任务,同步模式完全够用,而且写起来更省心。当你需要批量处理大量页面,或者构建复杂的测试套件时,再考虑将部分逻辑改造成异步,以提升执行速度。

1.2 浏览器上下文:实现测试隔离的利器

“浏览器上下文”这个概念是Playwright里非常核心和好用的一个功能,但新手很容易忽略它。你可以把它理解为一个完全独立的浏览器会话环境。每个上下文都有自己独立的缓存、Cookie、本地存储,就像你用浏览器的“无痕模式”新开了一个窗口一样。

为什么这很重要?想象一下测试场景:你第一个测试用例需要登录,第二个测试用例需要测试未登录状态。如果你所有操作都在同一个页面里进行,那么第一个测试留下的Cookie就会影响到第二个测试。传统做法可能需要你手动清理Cookie,非常麻烦。而使用浏览器上下文,你可以轻松实现隔离:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)

    # 创建第一个上下文和页面(模拟用户A)
    context1 = browser.new_context()
    page1 = context1.new_page()
    page1.goto("https://your-app.com/login")
    # ... 执行用户A的登录操作
    print("上下文1的Cookie:", context1.cookies())

    # 创建第二个上下文和页面(模拟用户B,完全独立)
    context2 = browser.new_context()
    page2 = context2.new_page()
    page2.goto("https://your-app.com")
    # 页面2是未登录状态,不受页面1影响
    print("上下文2的Cookie:", context2.cookies())

    # 分别关闭上下文
    context1.close()
    context2.close()
    browser.close()

这个特性太有用了。你可以用它来并行测试不同用户角色,或者确保每个测试用例都在一个干净的环境中开始。此外,浏览器上下文还能用来模拟不同的设备(如手机、平板)、语言时区、权限(地理位置、通知)等。只需要在创建上下文时传入参数即可,比如 context = browser.new_context(viewport={'width': 375, 'height': 667}, locale='zh-CN') 就能模拟一台中文的iPhone。善用浏览器上下文,能让你的测试脚本更加健壮和灵活。

2. 元素定位:稳、准、狠地找到页面上的目标

自动化测试的核心就是“模拟人操作浏览器”。人用眼睛找按钮,代码用什么找?就是用“元素定位”。定位不准,后续的所有点击、输入都是空谈。Playwright提供了十几种定位器,功能强大到令人发指,但咱们不用全学,掌握最核心的几种,就能解决95%的问题。我的经验是:优先使用有语义的定位器,万不得已再用CSS和XPath。

什么是有语义的定位器?就是Playwright内置的、根据元素在页面中的“角色”或“文本”来查找的方法。它们写出来的代码可读性极高,而且即使页面结构微调(比如CSS类名变了),只要元素的功能没变,你的测试脚本依然能运行。最常用的有这几个:

  • page.get_by_role(): 这是我最推荐的方法。它通过ARIA角色来定位元素,比如按钮(button)、链接(link)、文本框(textbox)。一个典型的搜索按钮可以这样定位:page.get_by_role("button", name="搜索")。意思就是“找一个角色是按钮,并且可访问名称是‘搜索’的元素”。这非常符合元素的本质功能。
  • page.get_by_text(): 根据元素内的可见文本定位。比如页面上有个提示语“操作成功”,你可以用 page.get_by_text("操作成功") 来定位它,并断言它是否存在。
  • page.get_by_label(): 通过关联的<label>标签文本来定位表单输入框。这是定位表单元素最稳健的方式。假设有一个标签为“用户名:”的输入框,用 page.get_by_label("用户名:") 就能精准定位。
  • page.get_by_placeholder(): 根据输入框内的占位符文本定位。比如 page.get_by_placeholder("请输入手机号")。
  • page.get_by_title(): 根据元素的title属性定位。

我们来看一个综合例子,模拟登录一个假设的网站:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto("https://demo.testfire.net/login.jsp")

    # 使用 get_by_label 定位用户名输入框(比用CSS选择器稳得多)
    page.get_by_label("Username").fill("admin")
    # 使用 get_by_label 定位密码输入框
    page.get_by_label("Password").fill("admin")
    # 使用 get_by_role 定位登录按钮
    page.get_by_role("button", name="Login").click()

    # 等待跳转,然后用 get_by_text 断言登录成功
    page.wait_for_url("**/bank/main.jsp")
    welcome_text = page.get_by_text("Welcome")
    if welcome_text.is_visible():
        print("登录成功断言通过!")
    else:
        print("登录可能失败了。")

    browser.close()

这段代码是不是读起来就像自然语言?这就是语义化定位器的魅力。当然,有些时候页面设计不规范,没有合适的角色或文本,我们就得祭出传统武器了:CSS选择器和XPath。page.locator() 是一个通用定位器,它接受一个CSS选择器或XPath字符串。CSS选择器更常用,因为它性能通常更好,写法也更简洁。比如 page.locator("#submit-btn") 定位id为submit-btn的按钮,page.locator(".primary-button") 定位class包含primary-button的按钮。

XPath功能强大但写起来复杂,容易因为页面结构微小变动而失效,除非必要,尽量少用。一个经验法则是:语义定位器 > CSS选择器(优先ID、特定class)> 复杂的CSS组合器 > 万不得已的XPath。在实际项目中,我通常会先用浏览器的开发者工具检查元素,看看它有没有良好的role、aria-label或者关联的<label>,如果有,毫不犹豫用语义定位器。如果没有,再考虑用locator配合CSS选择器。

2.1 等待的艺术:告别“No Node Found”错误

新手写Playwright脚本,十个有九个会卡在“元素找不到”的错误上。代码明明照着写的,一运行就报错 TimeoutError: locator.click: Timeout 30000ms exceeded. 或者 Error: No node found for selector: #kw。这绝大多数情况不是你的定位器写错了,而是时机不对。

网页是动态加载的。你的代码执行到 page.locator('#kw').click() 这一行时,可能那个搜索框的HTML元素还没被浏览器加载到DOM树里呢!你让Playwright去点一个不存在的东西,它当然会报错。所以,等待是自动化测试中最重要的概念之一。Playwright内置了“自动等待”机制,这比Selenium智能很多。对于大多数操作(如click, fill),Playwright在执行前会自动等待元素满足一系列条件:比如元素被附加到DOM、可见、可交互、稳定等。但这并不总是够用。

你需要主动使用等待策略。主要有三种:

  1. 硬性等待:page.wait_for_timeout(5000)。意思是“不管三七二十一,先等5秒再说”。这是最不推荐的方式,因为网络或机器快慢会导致等待时间不足或过长,让测试变得脆弱且缓慢。
  2. 等待导航:page.wait_for_url("**/search**") 或 page.wait_for_navigation()。在点击一个会导致页面跳转的链接或按钮后使用,确保新页面加载完成。
  3. 等待元素状态(推荐):这是最精准的方式。你可以让Playwright等待某个元素达到特定状态后再继续。
    • page.locator('#su').wait_for(state='visible'):等待搜索按钮变得可见。
    • page.locator('.loading-spinner').wait_for(state='hidden'):等待加载动画消失。
    • page.get_by_text('订单提交成功').wait_for():等待“订单提交成功”这段文本出现在页面上。

把上面的登录例子改进一下,加入更稳健的等待:

# ... 打开浏览器和页面 ...
page.goto("https://demo.testfire.net/login.jsp")

# 等待登录表单区域加载出来,再执行操作
page.locator("form[name='login']").wait_for(state="visible")

# 填充表单
page.get_by_label("Username").fill("admin")
page.get_by_label("Password").fill("admin")

# 点击登录按钮,并等待页面导航完成(跳转到主页面)
with page.expect_navigation():
    page.get_by_role("button", name="Login").click()

# 现在新页面已经加载好了,可以安全地进行断言
assert page.get_by_text("Welcome").is_visible()
print("登录流程完成且断言成功!")
# ...

养成在关键操作前后添加等待的习惯,你的脚本稳定性会大大提升。记住一个原则:用等待元素状态来代替固定的sleep时间。

2.2 处理动态元素与列表

现实中的网页充满了动态内容,比如一个商品列表,是通过Ajax加载的,数量不固定。如何定位并操作这些元素?Playwright的 locator 方法返回的不是一个元素,而是一个“定位器对象”,它代表一个查询。当你调用 .all() 方法时,它会获取当前所有匹配该查询的元素列表。

假设我们要获取百度搜索结果的标题(假设每个标题在一个<h3>标签里):

# 假设已经在百度页面并完成了搜索
# 等待至少一个搜索结果出现
page.locator("h3").first.wait_for()

# 获取所有匹配 h3 的元素
all_headings = page.locator("h3").all()
print(f"一共找到了 {len(all_headings)} 个结果标题")

for index, heading in enumerate(all_headings):
    # 获取每个标题的文本内容
    title = heading.inner_text()
    print(f"{index+1}. {title}")
    # 你甚至可以点击某个特定的结果
    # if "某个关键词" in title:
    #     heading.click()
    #     break

这里有几个要点:1) 在获取列表前,先用 .first.wait_for() 确保至少有一个元素已经加载,避免拿到空列表。2) .all() 返回的是一个元素句柄的列表,你可以遍历它。3) 对每个元素句柄,你可以调用 .inner_text(), .click(), .get_attribute() 等方法进行操作。

对于更复杂的动态内容,比如一个无限滚动的列表,你可能需要结合循环和等待,不断滚动直到没有新内容加载。Playwright提供了 page.evaluate() 方法,可以执行JavaScript代码来操作页面,比如 page.evaluate('window.scrollTo(0, document.body.scrollHeight)') 来滚动到页面底部。处理动态元素是进阶必备技能,多练习几次就能掌握其中的节奏。

3. 脚本生成与录制:让工具帮你写第一版代码

如果你对写代码还有畏难情绪,或者面对一个非常复杂的页面操作流程不知从何下手,Playwright提供了一个“作弊神器”——代码录制器。这个功能可以把你手动在浏览器里的操作,实时转换成Playwright代码。这不仅是新手的救命稻草,也是老手快速生成脚本原型、探索页面交互的利器。

启动录制器非常简单,不需要写任何Python代码。直接在命令行里输入:playwright codegen。回车后,它会自动打开两个窗口:一个是你指定(或默认Chromium)的浏览器,另一个是“代码生成器”窗口。接下来,你就像正常用户一样,在浏览器里操作:点击链接、输入文字、提交表单……你的每一个操作,都会实时在代码生成器窗口里变成一行行Python代码。

如果你想从一开始就录制某个特定网站,可以指定URL:playwright codegen https://www.baidu.com。更强大的是,你还可以指定用哪种浏览器、哪种模式(同步/异步)来生成代码。例如:playwright codegen --target python-async -b webkit https://www.example.com 这个命令会用WebKit浏览器打开示例网站,并生成异步模式的Python代码。

我录制了一个在百度搜索“Playwright”的简单流程,生成的代码大致如下:

from playwright.sync_api import Playwright, sync_playwright

def run(playwright: Playwright) -> None:
    browser = playwright.chromium.launch(headless=False)
    context = browser.new_context()
    page = context.new_page()
    page.goto("https://www.baidu.com/")
    page.locator("#kw").click()
    page.locator("#kw").fill("playwright")
    page.locator("#kw").press("Enter")
    # 这里可能会生成等待或查看结果的代码
    # ...

    context.close()
    browser.close()

with sync_playwright() as playwright:
    run(playwright)

看到没?连点击、输入、按回车键这些细节都记录下来了。生成的代码结构清晰,直接复制到你的编辑器里就能运行。但是,切记不要把录制的代码直接当作最终脚本。录制生成的代码通常比较“机械”和“脆弱”。它可能使用不太稳定的选择器(比如依赖自动生成的ID或复杂的层级结构),也缺乏必要的等待和断言。它的正确用法是:作为快速草稿。你可以先录制,得到一个基础的操作流程框架,然后在这个框架上进行“精装修”:

  1. 优化定位器:把生成的 page.locator("#kw") 审视一下,看看有没有更稳健的定位方式,比如 page.get_by_role("searchbox")(如果百度搜索框有正确角色的话,这里只是举例)。
  2. 添加智能等待:在页面跳转、元素出现等关键点,用 wait_for_url、wait_for_selector 替换掉可能存在的 time.sleep 或没有等待的情况。
  3. 补充断言:录制只记录操作,不记录预期结果。你需要手动添加断言,比如检查搜索后是否跳转到结果页,结果列表中是否包含预期关键词。
  4. 重构和模块化:将重复的操作(如登录)提取成函数,让代码更整洁。

把录制当作学习和快速启动的工具,而不是最终解决方案。亲手优化录制代码的过程,正是你深入理解Playwright工作原理的最佳途径。

3.1 进阶录制:处理认证与复杂状态

录制简单的公开页面很容易,但我们需要测试的往往是需要登录的系统,或者有复杂状态流转的流程。直接从头开始录制登录过程会把用户名密码也录进去,这既不安全,代码也不通用。Playwright录制器提供了解决之道。

你可以在启动录制器时,直接加载一个已经包含登录状态(Cookie、LocalStorage)的浏览器上下文。首先,你需要写一个小脚本,专门用于登录并保存状态:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    context = browser.new_context()
    page = context.new_page()

    # 执行登录操作
    page.goto("https://your-internal-app.com/login")
    page.get_by_label("邮箱").fill("your_email@company.com")
    page.get_by_label("密码").fill("your_password")
    page.get_by_role("button", name="登录").click()
    # 等待登录成功,比如跳转到仪表盘页面
    page.wait_for_url("**/dashboard")

    # 关键步骤:将当前上下文的状态保存到磁盘
    context.storage_state(path="auth_state.json")
    browser.close()

运行这个脚本,成功登录后,会在当前目录生成一个 auth_state.json 文件,里面包含了所有的Cookie、本地存储等信息。接下来,你就可以带着这个“身份”去录制了:playwright codegen --load-storage=auth_state.json https://your-internal-app.com/dashboard。这样启动的浏览器,直接就是登录状态,你可以从登录后的页面开始录制核心业务流程,而无需再处理登录。这既安全又高效,生成的代码也干净很多。

同样,对于多步骤流程(如下单),你可以先手动操作到某个中间状态(如购物车已有商品),然后保存这个状态。后续的测试脚本可以直接加载这个状态,从中间环节开始测试,节省了大量前置步骤的时间。这个技巧在编写数据依赖性强、流程长的测试用例时,价值巨大。

4. 跟踪与调试:当测试失败时,你不再是“盲人摸象”

写自动化脚本,最头疼的不是写不出来,而是运行失败时不知道哪里出了问题。页面上一个元素没加载?网络请求超时?脚本逻辑有误?传统方式你可能需要疯狂地加 print 语句,或者截一堆图。Playwright的跟踪功能彻底改变了这一点,它就像一个给测试过程安装的“黑匣子”,能记录下执行过程中的每一个细节。

跟踪功能可以记录下每一步操作执行前后的页面截图、DOM快照、网络请求、控制台日志,甚至是执行耗时。当测试失败时,你可以打开这个跟踪文件,像看视频回放一样,精确地定位到问题发生的瞬间。启用跟踪非常简单,只需要在创建浏览器上下文后,启动它,并在测试结束后停止并保存即可。

让我们改造一下最初的百度搜索例子,加入跟踪功能:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    # 创建上下文时,可以配置视口、用户代理等
    context = browser.new_context(viewport={'width': 1920, 'height': 1080})

    # 启动跟踪!snapshots=True 会记录DOM,screenshots=True 会记录截图
    context.tracing.start(snapshots=True, screenshots=True, sources=True)

    page = context.new_page()
    try:
        page.goto('https://www.baidu.com')
        print(page.title())
        page.locator('#kw').fill('自动化测试')
        page.locator('#su').click()
        # 等待结果出现,使用更精确的等待
        page.locator('div.result').first.wait_for(state='visible', timeout=10000)
        results = page.locator('h3').all()
        for r in results[:5]:  # 只打印前5个结果
            print(r.inner_text())
    except Exception as e:
        print(f"测试执行出错:{e}")
        # 即使出错,也保存跟踪文件,这对调试至关重要!
    finally:
        # 停止跟踪并将数据保存到文件
        context.tracing.stop(path = "my_trace.zip")
        browser.close()

运行这个脚本后,会在当前目录生成一个 my_trace.zip 文件。这个文件怎么查看?有两种方式。第一种是命令行方式:playwright show-trace my_trace.zip。这个命令会在本地启动一个简单的HTTP服务器,并自动在浏览器中打开跟踪查看器。第二种是使用在线工具:打开浏览器,访问 https://trace.playwright.dev/,直接把 my_trace.zip 文件拖进去。

打开跟踪文件后,你会看到一个强大的界面。顶部是一个时间轴,清晰地展示了脚本执行的每一步:goto, fill, click, wait_for_selector... 点击时间轴上的任何一个步骤,主视图区就会显示在这一步执行之后的页面截图。你不仅可以看,还可以互动:

  • 查看DOM:在“Snapshot”标签页,你可以看到这一步结束时页面的完整HTML结构,并且可以点击查看任何元素的详细属性。
  • 检查网络请求:在“Network”标签页,可以看到这一步触发或接收了哪些网络请求,状态码、耗时、响应内容一目了然。这对于调试API接口调用失败的问题极其有用。
  • 查看控制台日志:页面JavaScript输出的 console.log、error、warning 在这里都能看到。
  • 查看源码:因为我们在启动时设置了 sources=True,你甚至可以看到测试脚本本身的源代码,并与操作时间轴对应起来。

想象一个场景:你的测试在点击“提交订单”按钮后失败了。通过跟踪回放,你可以精确地看到点击按钮前那一刻,页面是什么样子(截图),按钮元素是否处于可点击状态(DOM),点击后发出了什么网络请求(Network),以及服务器返回了什么(响应)。可能你会发现,失败是因为一个意料之外的弹窗遮挡了按钮,或者网络请求返回了500错误。没有跟踪功能,你可能要花几个小时猜测和验证;有了它,几分钟就能定位根因。

4.1 高级调试技巧:与浏览器开发者工具联动

虽然跟踪功能很强大,但有时我们还需要进行更动态的调试,比如在脚本执行过程中,实时地检查变量、修改代码。Playwright提供了两种方式让你“暂停”脚本,直接使用浏览器的开发者工具。

第一种是在代码中插入 page.pause()。当脚本执行到这一行时,浏览器会暂停,并自动打开开发者工具。此时,你可以像调试普通网页一样,查看元素、控制台、网络,甚至可以在控制台里执行JavaScript来操作页面。检查完毕后,在Playwright Inspector窗口点击“Resume”即可继续执行。

第二种方式是在无头模式启动时,添加 --inspector 参数。如果你用命令行运行脚本,可以这样:python -m playwright run --inspector your_script.py。或者在代码启动浏览器时设置 devtools=True:browser = p.chromium.launch(headless=False, devtools=True)。这样浏览器打开时就会直接显示开发者工具。

我更常用的一个技巧是组合使用。在可能出问题的操作前加上 page.pause(),运行脚本。当暂停时,我不仅用开发者工具查看页面状态,还会利用跟踪功能已经记录的信息(因为跟踪是全程开启的),在跟踪查看器里前后滑动时间轴,对比问题发生前后的页面变化。这种“静态记录回放”加“动态实时调试”的组合拳,几乎能解决所有前端交互类的测试问题。记住,好的测试工程师不仅是写脚本的人,更是能快速定位和解决问题的侦探。跟踪和调试工具,就是你的放大镜和指纹采集器。

5. 实战项目:构建一个可复用的自动化测试小框架

学了一堆零散的知识点,是时候把它们串起来了。这一章,我们不写孤立的脚本,而是尝试构建一个简单但结构清晰的自动化测试小项目。目标是测试一个模拟的电商网站(例如 https://www.saucedemo.com/)的登录和购物车流程。你会学到如何组织代码、封装公共操作、管理测试数据,以及生成测试报告。

首先,规划一下项目结构。一个清晰的结构能让你的代码易于维护和扩展。

my_playwright_project/
├── pages/           # 页面对象模型
│   ├── __init__.py
│   ├── login_page.py
│   └── cart_page.py
├── tests/           # 测试用例
│   ├── __init__.py
│   ├── test_login.py
│   └── test_cart.py
├── fixtures/        # 测试固件(如浏览器初始化)
│   └── conftest.py
├── utils/           # 工具函数(如数据读取)
│   └── data_helper.py
├── reports/         # 测试报告输出目录
├── trace/           # 跟踪文件输出目录
└── requirements.txt # 项目依赖

第一步,创建页面对象模型。 这是现代自动化测试的核心设计模式。把每个页面(或页面中的重要组件)封装成一个类,页面上的元素定位和操作就是这个类的方法。这样,测试用例读起来就像在讲业务故事,而且页面UI一变,你只需要改这一个类文件。

pages/login_page.py 可能长这样:

from playwright.sync_api import Page

class LoginPage:
    def __init__(self, page: Page):
        self.page = page
        self.username_input = page.get_by_role("textbox", name="Username")
        self.password_input = page.get_by_role("textbox", name="Password")
        self.login_button = page.get_by_role("button", name="Login")

    def navigate(self):
        self.page.goto("https://www.saucedemo.com/")

    def login(self, username: str, password: str):
        self.username_input.fill(username)
        self.password_input.fill(password)
        self.login_button.click()
        # 等待登录成功,跳转到库存页面
        self.page.wait_for_url("**/inventory.html")

第二步,编写测试用例。 使用 pytest 这个流行的测试框架,它能方便地管理用例、夹具和生成报告。

tests/test_login.py:

import pytest
from pages.login_page import LoginPage

# 测试数据可以放在这里,或者从外部文件读取
TEST_DATA = [
    ("standard_user", "secret_sauce", True, "登录成功"),
    ("locked_out_user", "secret_sauce", False, "用户被锁定"),
    ("invalid_user", "invalid_pass", False, "凭证错误"),
]

@pytest.mark.parametrize("username, password, expected_success, description", TEST_DATA)
def test_login(setup_teardown, username, password, expected_success, description):
    """
    参数化测试:用多组数据测试登录功能
    """
    page, context = setup_teardown
    login_page = LoginPage(page)
    login_page.navigate()

    # 执行登录操作
    login_page.login(username, password)

    if expected_success:
        # 断言登录成功:URL包含inventory,且购物车图标可见
        assert "inventory" in page.url
        assert page.locator(".shopping_cart_link").is_visible()
        print(f"测试通过:{description}")
    else:
        # 断言登录失败:错误信息容器可见
        error_box = page.locator("[data-test='error']")
        error_box.wait_for(state="visible")
        assert error_box.is_visible()
        print(f"测试通过:{description} - 预期错误出现")

第三步,创建测试夹具。 夹具用来管理测试的“脚手架”,比如每个测试用例都需要一个新的浏览器页面。

fixtures/conftest.py:

import pytest
from playwright.sync_api import Playwright, BrowserContext, Page

@pytest.fixture(scope="function") # 每个测试函数执行一次
def setup_teardown(playwright: Playwright):
    """为每个测试提供全新的浏览器上下文和页面,并自动清理"""
    browser = playwright.chromium.launch(headless=True) # 无头模式,运行更快
    context = browser.new_context()
    # 启动跟踪,每个测试生成独立的跟踪文件
    context.tracing.start(screenshots=True, snapshots=True)
    page = context.new_page()
    yield page, context  # 将page和context提供给测试用例使用
    # 测试结束后执行清理
    context.tracing.stop(path=f"trace/test_trace_{pytest.current_test_name}.zip")
    context.close()
    browser.close()

@pytest.fixture(scope="session")
def playwright():
    """在整个测试会话中只启动一次Playwright实例"""
    from playwright.sync_api import sync_playwright
    with sync_playwright() as p:
        yield p

第四步,运行并生成报告。 在项目根目录下,安装pytest和pytest-html(用于生成HTML报告):pip install pytest pytest-html。然后运行测试:pytest tests/ -v --html=reports/report.html --self-contained-html。这条命令会运行tests目录下所有测试,并生成一个美观的、独立的HTML报告在reports文件夹里。报告中会清晰列出哪些用例通过,哪些失败,以及失败时的错误信息和截图(如果配置了)。

通过这个小项目,你将体验到从“写脚本”到“做工程”的转变。页面对象模式让代码更易读易维护;pytest夹具管理了复杂的资源生命周期;参数化测试让你能用多组数据验证功能;HTML报告让测试结果一目了然。更重要的是,这个框架是可扩展的。你可以轻松地加入新的页面类(如CheckoutPage),新的测试用例,甚至集成到CI/CD流水线中,每天自动运行。这才是将Playwright自动化测试投入实际生产的正确姿势。

更多推荐