 
                    简介
Playwright 是微软开源的端到端(end-to-end)测试框架,可用于现代 Web 应用。Playwright 提供如下特性:
1. 任意浏览器、任意平台、一种 API
跨浏览器:Playwright 支持所有现代渲染引擎,包括 Chromium、WebKit 和 Firefox。
跨平台:在 Windows、Linux 和 macOS 上,进行本地或 CI 测试(无头或有头)。
跨语言:可在 TypeScript、JavaScript、Python、.NET、Java 中使用 Playwright API。
测试移动 Web:Android Google Chrome 和移动 Safari 的本地移动仿真。桌面和云上运行的渲染引擎相同。
2. 弹性、没有古怪的测试
自动等待:Playwright 在执行操作前,将等待到元素可被操作。它还有一组丰富的检查事件。两者结合可消除对人为超时的需求 — 这是导致古怪测试的主要原因。
Web 优先断言:Playwright 断言是专门为动态 Web 创建的。检查将自动重试,直到满足必要的条件。
追踪:配置测试重试策略,捕获执行踪迹,录像,截屏,以防止遗忘。
复合一切:横跨多个选项卡、多个源和多个用户的测试场景。为不同用户创建具有不同上下文的场景,并且在服务器上运行它们,这些都在一个测试中进行。
可信事件:悬停元素,与动态控件交互,生成可信事件。Playwright 使用真正的浏览器输入管道,与真正的用户没有区别。
测试 Frame、穿透 Shadow DOM:Playwright 选择器穿透 Shadow DOM,允许无缝进入 Frame。
浏览器上下文:Playwright 为每个测试创建一个浏览器上下文。浏览器上下文等同于全新的浏览器配置文件。它提供零开销的完全测试隔离。创建新浏览器上下文仅需几毫秒。
登录一次:保存上下文的身份认证状态,并且在所有测试中重用。避免在每个测试中重复登录,还提供独立测试的完全隔离。
代码生成:通过录制操作生成测试。将它们保存成任何语言。
Playwright 检查器:检查页面,生成选择器,逐步完成测试执行,查看单击点,探索执行日志。
本文测试环境
操作系统:macOS 12.6
Python:3.10.6(下文以 Python 为例,进行讲述)
Playwright:1.30.0
安装
创建测试环境
mkdir playwright-democd playwright-demo/python3 -m venv venv# 安装 Pytest 插件venv/bin/pip3 install pytest-playwright# 安装需要的浏览器venv/bin/playwright install
添加样例测试
在当前工作目录或子目录内部,创建 test_my_application.py 文件,其内容如下:
import refrom playwright.sync_api import Page, expectdef test_homepage_has_Playwright_in_title_and_get_started_link_linking_to_the_intro_page(page: Page):page.goto("https://playwright.dev/")# Expect a title "to contain" a substring.expect(page).to_have_title(re.compile("Playwright"))# create a locatorget_started = page.locator("text=Get Started")# Expect an attribute "to be strictly equal" to the value.expect(get_started).to_have_attribute("href", "/docs/intro")# Click the get started link.get_started.click()# Expects the URL to contain intro.expect(page).to_have_url(re.compile(".*intro"))
运行样例测试
在默认情况下,测试运行在 Chromium 上,可通过 CLI 选项进行配置,测试以 Headless 模式运行。测试结果和测试日志被展示在终端中。
venv/bin/pytest编写测试
下面是展示如何编写使用断言、定位器(locator)和选择器(selector)的测试的示例。
import refrom playwright.sync_api import Page, expectdef test_homepage_has_Playwright_in_title_and_get_started_link_linking_to_the_intro_page(page: Page):page.goto("https://playwright.dev/")# Expect a title "to contain" a substring.expect(page).to_have_title(re.compile("Playwright"))# create a locatorget_started = page.locator("text=Get Started")# Expect an attribute "to be strictly equal" to the value.expect(get_started).to_have_attribute("href", "/docs/intro")# Click the get started link.get_started.click()# Expects the URL to contain intro.expect(page).to_have_url(re.compile(".*intro"))
断言
Playwright 提供 expect (https://playwright.dev/python/docs/test-assertions)函数,它将一直等待,直到满足预期条件。
import refrom playwright.sync_api import expectexpect(page).to_have_title(re.compile("Playwright"))
定位器
from playwright.sync_api import expectget_started = page.locator("text=Get Started")expect(get_started).to_have_attribute("href", "/docs/installation")get_started.click()
选择器(Selector)是用于创建定位器的字符串。Playwright 支持许多不同的选择器,比如 Text、CSS、XPath 等。阅读 in-depth guide 文档,了解更多关于可用的选择器以及如何进行选择的信息。
from playwright.sync_api import expectexpect(page.locator("text=Installation")).to_be_visible()
测试隔离
from playwright.sync_api import Pagedef test_basic_test(page: Page):# ...
使用测试钩子
import pytestfrom playwright.sync_api import Page, expect@pytest.fixture(scope="function", autouse=True)def before_each_after_each(page: Page):print("beforeEach")# Go to the starting url before each test.page.goto("https://playwright.dev/")yieldprint("afterEach")def test_main_navigation(page: Page):# Assertions use the expect API.expect(page).to_have_url("https://playwright.dev/")
运行测试
在 Chromium 上运行测试
pytestpytest test_login.py运行一组测试文件
pytest tests/todo-page/ tests/landing-page/pytest -k "test_add_a_todo_item"pytest --headed test_login.pypytest test_login.py --browser webkitpytest test_login.py --browser webkit --browser firefox并行运行测试
pytest --numprocesses auto【假定已安装 pytest-xdist,查看 here (https://playwright.dev/python/docs/test-runners#parallelism-running-multiple-tests-at-once)获取更多信息。】
运行测试
PWDEBUG=1 pytest -s查看调试指南(debugging guide)了解关于 Playwright Inspector 以及使用浏览器开发者工具(Browser Developer tools)进行调试的更多信息。
测试生成器
运行 Codegen
playwright codegen playwright.dev使用 clear 按钮清除代码,重新开始录制。完成时,关闭 Playwright Inspector 窗口,或停止终端命令。
追踪查看器(Trace Viewer)
Playwright 追踪查看器是一个 GUI 工具,它使你可以探查测试中记录的 Playwright 追踪,可以在测试的每个操作中来回移动,可视化地查看每个操作期间正在发生什么。
你将学习:
如何记录追踪
如何打开 HTML 报告
如何打开追踪查看器
记录追踪
像下面一样使用 browser_context.tracing API 记录追踪:
browser = chromium.launch()context = browser.new_context()# Start tracing before creating / navigating a page.context.tracing.start(screenshots=True, snapshots=True, sources=True)page.goto("https://playwright.dev")# Stop tracing and export it into a zip archive.context.tracing.stop(path = "trace.zip")
这将记录追踪,把它导出到名称为 trace.zip 的文件中。
打开追踪
playwright show-trace trace.zip查看追踪
Pytest 插件参考
Playwright 提供 Pytest 插件,来编写端到端的测试。如果想开始使用它,请参考 getting started guide。
用法
使用 Pytest(https://docs.pytest.org/en/stable/) CLI 运行测试:
pytest --browser webkit --headed如果你想自动地添加 CLI 参数,而不是指定它们,请使用 pytest.ini 文件。
CLI 参数
Fixture
该插件为 pytest 配置 Playwright 特定的 fixture。为使用这些 fixture,使用 fixture 名称作为测试函数的参数。
def test_my_app_is_working(fixture_name):# Test using fixture_name# ...
函数作用域:这些 fixture 在测试函数请求时创建,在测试结束时销毁。
context:用于测试的新浏览器上下文(browser context)
会话作用域:这些 fixture 在测试函数请求时创建,在测试结束时销毁。
playwright:Playwright 实例
browser_type:当前浏览器的 BrowserType 实例
browser:Playwright 启动的 Browser 实例
browser_name:浏览器名称
browser_channel:浏览器通道(channel)
自定义 fixture 选项:对于 browser 和 context fixture,使用下面的 fixture 来定义自定义启动选项。
browser_type_launch_args:重写用于 browser_type.launch(**kwargs) 的启动参数。它应该返回字典
browser_context_args:重写用于 browser.new_context(**kwargs) 的选项。它应该返回字典
并行:同时运行多个测试
如果测试运行在有多个 CPU 的机器上,可以通过使用 pytest-xdist 同时运行多个测试,加快测试套件的整体执行时间。
# install dependencypip install pytest-xdist# use the --numprocesses flagpytest --numprocesses auto
根据硬件和测试的特性,可以将 numprocesses 设置为 2 到机器上 CPU 数量之间的任意值。如果设置得过高,可能产生非预期行为。
示例
配置 Mypy 类型以自动补全
# test_my_application.pyfrom playwright.sync_api import Pagedef test_visit_admin_dashboard(page: Page):page.goto("/admin")# ...
配置慢动作
使用 --slowmo 参数以慢动作运行测试。
pytest --slowmo 100通过浏览器跳过测试
# test_my_application.pyimport pytest@pytest.mark.skip_browser("firefox")def test_visit_example(page):page.goto("https://example.com")# ...
在特定的浏览器上运行测试
# conftest.pyimport pytest@pytest.mark.only_browser("chromium")def test_visit_example(page):page.goto("https://example.com")# ...
使用自定义的浏览器通道运行
pytest --browser-channel chrome# test_my_application.pydef test_example(page):page.goto("https://example.com")
配置 base-url
使用 base-url 参数启动 Pytest。pytest-base-url (https://github.com/pytest-dev/pytest-base-url)插件允许通过配置、CLI 参数或像 fixture 一样设置 base url。
pytest --base-url http://localhost:8080# test_my_application.pydef test_visit_example(page):page.goto("/admin")# -> Will result in http://localhost:8080/admin
忽略 HTTPS 错误
# conftest.pyimport pytest@pytest.fixture(scope="session")def browser_context_args(browser_context_args):return {**browser_context_args,"ignore_https_errors": True}
使用自定义窗口大小
# conftest.pyimport pytest@pytest.fixture(scope="session")def browser_context_args(browser_context_args):return {**browser_context_args,"viewport": {"width": 1920,"height": 1080,}}
# conftest.pyimport pytest@pytest.fixture(scope="session")def browser_context_args(browser_context_args, playwright):iphone_11 = playwright.devices['iPhone 11 Pro']return {**browser_context_args,**iphone_11,}
或通过命令行 --device="iPhone 11 Pro"。
持久化上下文
# conftest.pyimport pytestfrom playwright.sync_api import BrowserTypefrom typing import Dict@pytest.fixture(scope="session")def context(browser_type: BrowserType,browser_type_launch_args: Dict,browser_context_args: Dict):context = browser_type.launch_persistent_context("./foobar", **{**browser_type_launch_args,**browser_context_args,"locale": "de-DE",})yield contextcontext.close()
从持久化上下文创建测试内部的所有页面。
与 unittest.TestCase 一起使用
参考下面的示例,了解如何与 unittest.TestCase 一起使用。这有一个限制,仅能指定一个浏览器,并且在指定多个浏览器时,不会生成多个浏览器的矩阵。
import pytestimport unittestfrom playwright.sync_api import Pageclass MyTest(unittest.TestCase):@pytest.fixture(autouse=True)def setup(self, page: Page):self.page = pagedef test_foobar(self):self.page.goto("https://microsoft.com")self.page.locator("#foobar").click()assert self.page.evaluate("1 + 1") == 2
调试
在代码中使用 breakpoint() 语句停止执行,获取 pdb REPL。
def test_bing_is_working(page):page.goto("https://bing.com")breakpoint()# ...
部署到 CI
认证
Playwright 可用于需要认证的自动化场景。
注意:本指南覆盖 cookie/token-based 认证(通过 app UI 登陆)。对于 HTTP 认证(HTTP authentication:https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication),请使用 browser.new_context(**kwargs):https://playwright.dev/python/docs/api/class-browser#browser-new-context。 
自动化登录
page = context.new_page()page.goto('https://github.com/login')# Interact with login formpage.get_by_text("Login").click()page.get_by_label("User Name").fill(USERNAME)page.get_by_label("Password").fill(PASSWORD)page.get_by_text('Submit').click()# Continue with the test
为每次测试重做登录将减慢测试的执行速度。为缓解这种情况,应该重用现有的认证状态。
重用签入状态
# Save storage state into the file.storage = context.storage_state(path="state.json")# Create a new context with the saved storage state.context = browser.new_context(storage_state="state.json")
Session Storage
Session Storage 很少用于存储与登陆状态相关的信息。Session Storage 特定于特定的域,页面加载时它不会持久化。Playwright 未提供持久化 Session Storage 的 API,但下面的片段可用于保存/加载 Session Storage。
import os# Get session storage and store as env variablesession_storage = page.evaluate("() => JSON.stringify(sessionStorage)")os.environ["SESSION_STORAGE"] = session_storage# Set session storage in a new contextsession_storage = os.environ["SESSION_STORAGE"]context.add_init_script("""(storage => {if (window.location.hostname === 'example.com') {const entries = JSON.parse(storage)for (const [key, value] of Object.entries(entries)) {window.sessionStorage.setItem(key, value)}}})('""" + session_storage + "')")
多因子认证
使用多因子认证(MFA)的账户无法完全自动化,需要人工干预。持久化认证可用于部分自动化 MFA 场景。
from playwright.sync_api import sync_playwrightwith sync_playwright() as p:user_data_dir = '/path/to/directory'browser = p.chromium.launch_persistent_context(user_data_dir, headless=False)# Execute login steps manually in the browser window
3. 重用用户数据目录来运行自动化场景
事件
Playwright 允许监听发生在 Web 页面上的多种类型的事件,比如网络请求、子页面的创建、专用 Worker 等。可通过多种方式订阅这些事件,比如等待事件或添加/移除事件监听者。
等待事件
with page.expect_request("**/*logo*.png") as first:page.goto("https://wikipedia.org")print(first.value.url)
等待弹出窗口:
with page.expect_popup() as popup:page.evaluate("window.open()")popup.value.goto("https://wikipedia.org")
添加/移除事件监听者
有时,事件发生在随机时间,而不是等待它们,它们需要被处理。Playwright 支持订阅/取消订阅事件的传统语言机制:
def print_request_sent(request):print("Request sent: " + request.url)def print_request_finished(request):print("Request finished: " + request.url)page.on("request", print_request_sent)page.on("requestfinished", print_request_finished)page.goto("https://wikipedia.org")page.remove_listener("requestfinished", print_request_finished)page.goto("https://www.openstreetmap.org/")
添加一次性监听者
如果特定事件需要被处理一次,那么可以使用便捷的 API:
page.once("dialog", lambda dialog: dialog.accept("2021"))page.evaluate("prompt('Enter a number:')")
API 测试
编写 API 测试
Github API 需要认证,因此我们将为所有测试配置一次 token。同时,我们也将设置 baseURL 以简化测试。
import osfrom typing import Generatorimport pytestfrom playwright.sync_api import Playwright, APIRequestContextGITHUB_API_TOKEN = os.getenv("GITHUB_API_TOKEN")assert GITHUB_API_TOKEN, "GITHUB_API_TOKEN is not set"@pytest.fixture(scope="session")def api_request_context(playwright: Playwright,) -> Generator[APIRequestContext, None, None]:headers = {# We set this header per GitHub guidelines."Accept": "application/vnd.github.v3+json",# Add authorization token to all requests.# Assuming personal access token available in the environment."Authorization": f"token {GITHUB_API_TOKEN}",}request_context = playwright.request.new_context(base_url="https://api.github.com", extra_http_headers=headers)yield request_contextrequest_context.dispose()
现在我们已初始化请求对象,我们可以添加一些在仓库中创建新 issue 的测试。
import osfrom typing import Generatorimport pytestfrom playwright.sync_api import Playwright, APIRequestContextGITHUB_API_TOKEN = os.getenv("GITHUB_API_TOKEN")assert GITHUB_API_TOKEN, "GITHUB_API_TOKEN is not set"GITHUB_USER = os.getenv("GITHUB_USER")assert GITHUB_USER, "GITHUB_USER is not set"GITHUB_REPO = "test"# ...def test_should_create_bug_report(api_request_context: APIRequestContext) -> None:data = {"title": "[Bug] report 1","body": "Bug description",}new_issue = api_request_context.post(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues", data=data)assert new_issue.okissues = api_request_context.get(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues")assert issues.okissues_response = issues.json()issue = list(filter(lambda issue: issue["title"] == "[Bug] report 1", issues_response))[0]assert issueassert issue["body"] == "Bug description"def test_should_create_feature_request(api_request_context: APIRequestContext) -> None:data = {"title": "[Feature] request 1","body": "Feature description",}new_issue = api_request_context.post(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues", data=data)assert new_issue.okissues = api_request_context.get(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues")assert issues.okissues_response = issues.json()issue = list(filter(lambda issue: issue["title"] == "[Feature] request 1", issues_response))[0]assert issueassert issue["body"] == "Feature description"
# ...@pytest.fixture(scope="session", autouse=True)def create_test_repository(api_request_context: APIRequestContext,) -> Generator[None, None, None]:# Before allnew_repo = api_request_context.post("/user/repos", data={"name": GITHUB_REPO})assert new_repo.okyield# After alldeleted_repo = api_request_context.delete(f"/repos/{GITHUB_USER}/{GITHUB_REPO}")assert deleted_repo.ok
这是 API 测试的完整样例:
from enum import autoimport osfrom typing import Generatorimport pytestfrom playwright.sync_api import Playwright, Page, APIRequestContext, expectGITHUB_API_TOKEN = os.getenv("GITHUB_API_TOKEN")assert GITHUB_API_TOKEN, "GITHUB_API_TOKEN is not set"GITHUB_USER = os.getenv("GITHUB_USER")assert GITHUB_USER, "GITHUB_USER is not set"GITHUB_REPO = "test"@pytest.fixture(scope="session")def api_request_context(playwright: Playwright,) -> Generator[APIRequestContext, None, None]:headers = {# We set this header per GitHub guidelines."Accept": "application/vnd.github.v3+json",# Add authorization token to all requests.# Assuming personal access token available in the environment."Authorization": f"token {GITHUB_API_TOKEN}",}request_context = playwright.request.new_context(base_url="https://api.github.com", extra_http_headers=headers)yield request_contextrequest_context.dispose()@pytest.fixture(scope="session", autouse=True)def create_test_repository(api_request_context: APIRequestContext,) -> Generator[None, None, None]:# Before allnew_repo = api_request_context.post("/user/repos", data={"name": GITHUB_REPO})assert new_repo.okyield# After alldeleted_repo = api_request_context.delete(f"/repos/{GITHUB_USER}/{GITHUB_REPO}")assert deleted_repo.okdef test_should_create_bug_report(api_request_context: APIRequestContext) -> None:data = {"title": "[Bug] report 1","body": "Bug description",}new_issue = api_request_context.post(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues", data=data)assert new_issue.okissues = api_request_context.get(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues")assert issues.okissues_response = issues.json()issue = list(filter(lambda issue: issue["title"] == "[Bug] report 1", issues_response))[0]assert issueassert issue["body"] == "Bug description"def test_should_create_feature_request(api_request_context: APIRequestContext) -> None:data = {"title": "[Feature] request 1","body": "Feature description",}new_issue = api_request_context.post(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues", data=data)assert new_issue.okissues = api_request_context.get(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues")assert issues.okissues_response = issues.json()issue = list(filter(lambda issue: issue["title"] == "[Feature] request 1", issues_response))[0]assert issueassert issue["body"] == "Feature description"
通过 API 调用准备服务端状态
下面的测试通过 API 创建新 issue,然后导航到项目中所有 issue 的列表,检查它是否出现在列表的顶部。使用 LocatorAssertions 执行该检查。
def test_last_created_issue_should_be_first_in_the_list(api_request_context: APIRequestContext, page: Page) -> None:def create_issue(title: str) -> None:data = {"title": title,"body": "Feature description",}new_issue = api_request_context.post(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues", data=data)assert new_issue.okcreate_issue("[Feature] request 1")create_issue("[Feature] request 2")page.goto(f"https://github.com/{GITHUB_USER}/{GITHUB_REPO}/issues")first_issue = page.locator("a[data-hovercard-type='issue']").firstexpect(first_issue).to_have_text("[Feature] request 2")
在运行用户操作后,检查服务器状态
下面的测试通过浏览器中的用户接口创建新 issue,然后通过 API 检查它是否被创建。
def test_last_created_issue_should_be_on_the_server(api_request_context: APIRequestContext, page: Page) -> None:page.goto(f"https://github.com/{GITHUB_USER}/{GITHUB_REPO}/issues")page.locator("text=New issue").click()page.locator("[aria-label='Title']").fill("Bug report 1")page.locator("[aria-label='Comment body']").fill("Bug description")page.locator("text=Submit new issue").click()issue_id = page.url.split("/")[-1]new_issue = api_request_context.get(f"https://github.com/{GITHUB_USER}/{GITHUB_REPO}/issues/{issue_id}")assert new_issue.okassert new_issue.json()["title"] == "[Bug] report 1"assert new_issue.json()["body"] == "Bug description"
重用认证状态
request_context = playwright.request.new_context(http_credentials={"username": "test", "password": "test"})request_context.get("https://api.example.com/login")# Save storage state into a variable.state = request_context.storage_state()# Create a new context with the saved storage state.context = browser.new_context(storage_state=state)
关于Portal Lab
星阑科技 Portal Lab 致力于前沿安全技术研究及能力工具化。主要研究方向为API 安全、应用安全、攻防对抗等领域。实验室成员研究成果曾发表于BlackHat、HITB、BlueHat、KCon、XCon等国内外知名安全会议,并多次发布开源安全工具。未来,Portal Lab将继续以开放创新的态度积极投入各类安全技术研究,持续为安全社区及企业级客户提供高质量技术输出。