freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

UI 和 API 自动化测试神器 - Playwright
2023-02-21 15:25:58
所属地 北京

简介

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 创建的。检查将自动重试,直到满足必要的条件。

  • 追踪:配置测试重试策略,捕获执行踪迹,录像,截屏,以防止遗忘。

3. 无需折中、无限制:浏览器在不同进程中运行属于不同源的 Web 内容。Playwright 与现代浏览器架构保持一致,在进程外运行测试。这使得 Playwright 摆脱典型的进程内测试运行器限制。

  • 复合一切:横跨多个选项卡、多个源和多个用户的测试场景。为不同用户创建具有不同上下文的场景,并且在服务器上运行它们,这些都在一个测试中进行。

  • 可信事件:悬停元素,与动态控件交互,生成可信事件。Playwright 使用真正的浏览器输入管道,与真正的用户没有区别。

  • 测试 Frame、穿透 Shadow DOM:Playwright 选择器穿透 Shadow DOM,允许无缝进入 Frame。

4. 完全隔离、快速执行:

  • 浏览器上下文:Playwright 为每个测试创建一个浏览器上下文。浏览器上下文等同于全新的浏览器配置文件。它提供零开销的完全测试隔离。创建新浏览器上下文仅需几毫秒。

  • 登录一次:保存上下文的身份认证状态,并且在所有测试中重用。避免在每个测试中重复登录,还提供独立测试的完全隔离。

5. 强大的工具:

  • 代码生成:通过录制操作生成测试。将它们保存成任何语言。

  • Playwright 检查器:检查页面,生成选择器,逐步完成测试执行,查看单击点,探索执行日志。

  • 追踪查看器:捕获所有信息以调查测试失败。Playwright 追踪包含测试执行录屏、实时 DOM 快照、操作资源管理器、测试源等。

本文测试环境

  • 操作系统:macOS 12.6

  • Python:3.10.6(下文以 Python 为例,进行讲述)

  • Playwright:1.30.0

安装

创建测试环境

mkdir playwright-demo cd playwright-demo/ python3 -m venv venv # 安装 Pytest 插件 venv/bin/pip3 install pytest-playwright # 安装需要的浏览器 venv/bin/playwright install

添加样例测试

在当前工作目录或子目录内部,创建 test_my_application.py 文件,其内容如下:

import re
from playwright.sync_api import Page, expect


def 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 locator
    get_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

编写测试

Playwright 断言(assertion)是专门为动态网页创建的。检查将自动重试,直到满足必要的条件。Playwright 自带 auto-wait,这意味着它在执行操作前,等待到元素变为可操作的(actionable)。Playwright 提供 expect 函数来编写断言。

下面是展示如何编写使用断言、定位器(locator)和选择器(selector)的测试的示例。

import re
from playwright.sync_api import Page, expect


def 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 locator
    get_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 re
from playwright.sync_api import expect

expect(page).to_have_title(re.compile("Playwright"))

定位器

定位器(Locator)是 Playwright 的自动等待和重试能力的核心部分。定位器是一种随时在网页上查找元素的方法,用于在元素上执行诸如 .click、.fill 之类的操作。可以使用 page.locator(selector, **kwargs) 方法创建自定义定位器。

from playwright.sync_api import expect get_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 expect expect(page.locator("text=Installation")).to_be_visible()

测试隔离

Playwright Pytest 插件基于 test fixture(比如 built in page fixture)的概念,它将被传给你的测试。由于浏览器上下文,在测试之间,页面(Page)彼此隔离,这相当于开启新的浏览器行为,每个测试获得新环境,即使在一个浏览器中运行多个测试时,也是如此。

from playwright.sync_api import Page

def test_basic_test(page: Page):
  # ...

使用测试钩子

可以使用各种各样的 fixture 在测试之前或之后执行代码,以及在它们之间共享对象。函数(function)作用域的 fixture 具有 beforeEach/afterEach 一样的自动使用行为。模块(module)作用域的 fixture 具有 beforeAll/afterAll 一样的自动使用行为,它在所有测试之前和所有测试之后运行。

import pytest
from 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/")
    yield
    print("afterEach")

def test_main_navigation(page: Page):
    # Assertions use the expect API.
    expect(page).to_have_url("https://playwright.dev/")

运行测试

可以运行单个测试、一组测试或全部测试。测试可以运行在一种或多种浏览器上。默认情况下,以 Headless 方式运行测试,这意味着在运行测试时,不会打开浏览器窗口,可以在终端中看到结果。通过使用 --headed 标记,可以以 headed 模式运行测试。

  • 在 Chromium 上运行测试
pytest
  • 运行单个测试文件
pytest test_login.py
  • 运行一组测试文件

pytest tests/todo-page/ tests/landing-page/
  • 使用函数名运行测试

pytest -k "test_add_a_todo_item"
  • 以有头(headed)模式运行测试

pytest --headed test_login.py
  • 在指定的浏览器上运行测试

pytest test_login.py --browser webkit
  • 在多种浏览器上运行测试

pytest 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)获取更多信息。】

运行测试

因为 Playwright 运行在 Python 中,所以可以使用 Debugger 调试它。Playwright 自带 Playwright Inspector,它允许你逐步通过 Playwright API 调用,查看它们的调试日志,以及探索选择器(selectors)。

PWDEBUG=1 pytest -s

查看调试指南(debugging guide)了解关于 Playwright Inspector 以及使用浏览器开发者工具(Browser Developer tools)进行调试的更多信息。

测试生成器

Playwright 具有开箱即用的生成测试的能力,这是快速开始测试的好方法。它会打开两个窗口,一个是浏览器窗口,通过它你可以与希望测试的网站进行交互,另一个是 Playwright Inspector 窗口,通过它你可以录制测试、拷贝测试、清除测试以及改变测试的语言。

你将学习:

  • How to generate tests with Codegen

运行 Codegen

playwright codegen playwright.dev

运行 codegen,然后在浏览器中执行操作。Playwright 将为用户的交互生成代码。Codegen 尝试生成弹性的基于文本的选择器。

当你完成与页面的交互时,按下 record 按钮停止录制,使用 copy 按钮把生成的代码拷贝到编辑器。

使用 clear 按钮清除代码,重新开始录制。完成时,关闭 Playwright Inspector 窗口,或停止终端命令。

如欲了解有关生成测试的更多信息,请查看 Codegen 的详细指南。

追踪查看器(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 CLI 打开保存的追踪。

playwright show-trace trace.zip

查看追踪

通过单击每个操作或使用时间轴悬停,查看测试的追踪,以及查看操作前后的页面状态。在测试的每个步骤期间查看日志、源和网络。追踪查看器创建 DOM 快照,因此你可以与它进行交互,打开开发者工具(devtools)等。

如欲了解更多信息,请查看 Trace Viewer 的详细指南。

Pytest 插件参考

Playwright 提供 Pytest 插件,来编写端到端的测试。如果想开始使用它,请参考 getting started guide。

用法

使用 Pytest(https://docs.pytest.org/en/stable/) CLI 运行测试:

pytest --browser webkit --headed

如果你想自动地添加 CLI 参数,而不是指定它们,请使用 pytest.ini 文件。

CLI 参数

  • --headed:以有头模式运行测试(默认:无头)

  • --browser:用不同的浏览器 chromium、firefox、webkit 运行测试。可以指定多次(默认:所有浏览器)

  • --browser-channel:使用的 Browser channel

  • --slow-mo:使用慢动作运行测试

  • --device:要模拟的设备(Device)

  • --output:用于测试生成的制品(aritifact)的目录(默认:test-results)

  • --tracing:是否为每次测试记录追踪(trace:https://playwright.dev/python/docs/trace-viewer)。on、off 或 retain-on-failure(默认:off)

  • --video:是否为每次测试录制视频。on、off 或 retain-on-failure(默认:off)

  • --screenshot:是否在每次测试后,自动地捕获截图。on, off, or only-on-failure (默认:off)

Fixture

该插件为 pytest 配置 Playwright 特定的 fixture。为使用这些 fixture,使用 fixture 名称作为测试函数的参数。

def test_my_app_is_working(fixture_name):
    # Test using fixture_name
    # ...

函数作用域:这些 fixture 在测试函数请求时创建,在测试结束时销毁。

  • context:用于测试的新浏览器上下文(browser context)

  • page:用于测试的新浏览器页面(browser page)

会话作用域:这些 fixture 在测试函数请求时创建,在测试结束时销毁。

  • playwright:Playwright 实例

  • browser_type:当前浏览器的 BrowserType 实例

  • browser:Playwright 启动的 Browser 实例

  • browser_name:浏览器名称

  • browser_channel:浏览器通道(channel)

  • is_chromium、is_webkit、is_firefox:各自浏览器类型的布尔值

自定义 fixture 选项:对于 browser 和 context fixture,使用下面的 fixture 来定义自定义启动选项。

  • browser_type_launch_args:重写用于 browser_type.launch(**kwargs) 的启动参数。它应该返回字典

  • browser_context_args:重写用于 browser.new_context(**kwargs) 的选项。它应该返回字典

并行:同时运行多个测试

如果测试运行在有多个 CPU 的机器上,可以通过使用 pytest-xdist 同时运行多个测试,加快测试套件的整体执行时间。

# install dependency
pip install pytest-xdist
# use the --numprocesses flag
pytest --numprocesses auto

根据硬件和测试的特性,可以将 numprocesses 设置为 2 到机器上 CPU 数量之间的任意值。如果设置得过高,可能产生非预期行为。

有关 pytest 选项的常用信息,请参考 Running Tests。

示例

配置 Mypy 类型以自动补全

# test_my_application.py
from playwright.sync_api import Page

def test_visit_admin_dashboard(page: Page):
    page.goto("/admin")
    # ...

配置慢动作

使用 --slowmo 参数以慢动作运行测试。

pytest --slowmo 100

通过浏览器跳过测试

# test_my_application.py
import pytest

@pytest.mark.skip_browser("firefox")
def test_visit_example(page):
    page.goto("https://example.com")
    # ...

在特定的浏览器上运行测试

# conftest.py
import pytest

@pytest.mark.only_browser("chromium")
def test_visit_example(page):
    page.goto("https://example.com")
    # ...

使用自定义的浏览器通道运行

pytest --browser-channel chrome
# test_my_application.py
def 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.py
def test_visit_example(page):
    page.goto("/admin")
    # -> Will result in http://localhost:8080/admin

忽略 HTTPS 错误

# conftest.py
import pytest

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    return {
        **browser_context_args,
        "ignore_https_errors": True
    }

使用自定义窗口大小

# conftest.py
import pytest

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    return {
        **browser_context_args,
        "viewport": {
            "width": 1920,
            "height": 1080,
        }
    }

设备仿真

# conftest.py
import 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.py
import pytest
from playwright.sync_api import BrowserType
from 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 context
    context.close()

从持久化上下文创建测试内部的所有页面。

与 unittest.TestCase 一起使用

参考下面的示例,了解如何与 unittest.TestCase 一起使用。这有一个限制,仅能指定一个浏览器,并且在指定多个浏览器时,不会生成多个浏览器的矩阵。

import pytest
import unittest

from playwright.sync_api import Page


class MyTest(unittest.TestCase):
    @pytest.fixture(autouse=True)
    def setup(self, page: Page):
        self.page = page

    def test_foobar(self):
        self.page.goto("https://microsoft.com")
        self.page.locator("#foobar").click()
        assert self.page.evaluate("1 + 1") == 2

调试 

使用 pdb

在代码中使用 breakpoint() 语句停止执行,获取 pdb REPL。

def test_bing_is_working(page):
    page.goto("https://bing.com")
    breakpoint()
    # ...

部署到 CI

请查看 guides for CI providers 获取关于将测试部署到 CI/CD 的信息。

认证

Playwright 可用于需要认证的自动化场景。

使用 Playwright 编写的测试在被称为浏览器上下文(browser contexts)的、独立的、干净的环境中执行。这种隔离模型可以提升复现性,防止级联测试失败。新浏览器上下文可以加载现有的认证状态。这可以消除在每个上下文中登录的需求,加快测试执行的速度。

注意:本指南覆盖 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。

自动化登录

Playwright API 可以与登陆表单自动化交互(automate interaction)。

下面的示例自动化登陆到 Github。执行这些步骤后,浏览器上下文将被认证。

page = context.new_page()
page.goto('https://github.com/login')

# Interact with login form
page.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

为每次测试重做登录将减慢测试的执行速度。为缓解这种情况,应该重用现有的认证状态。

重用签入状态

Playwright 提供在测试中重用签入(signed-in)状态的方式。通过该方式,可以只登陆一次,然后跳过所有测试的登陆步骤。

Web 应用使用基于 Cookie 或基于 Token 的认证,认证状态被当作 Cookie 存储,或被存储在 Local Storage 中。Playwright 提供 browserContext.storageState(options) 方法,可使用它从已认证上下文中获取存储状态,然后使用预填充状态创建新上下文。

Cookie 和 Local Storage 状态可以跨不同的浏览器使用。它们依赖应用程序的认证模型:有些应用程序可能同时需要 Cookie 和 Local Storage。

下面的代码片段从已认证上下文中获取状态,然后使用该状态创建新上下文。

# 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 variable
session_storage = page.evaluate("() => JSON.stringify(sessionStorage)")
os.environ["SESSION_STORAGE"] = session_storage

# Set session storage in a new context
session_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 场景。

持久化认证

注意:持久化认证不适用于 CI 环境,因为它依赖磁盘位置。用户数据目录特定于浏览器类型,不能跨浏览器类型共享。

用户数据目录可以与 browser_type.launch_persistent_context(user_data_dir, **kwargs) API 一起使用。

from playwright.sync_api import sync_playwright

with 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

事件

Playwright 允许监听发生在 Web 页面上的多种类型的事件,比如网络请求、子页面的创建、专用 Worker 等。可通过多种方式订阅这些事件,比如等待事件或添加/移除事件监听者。

等待事件

大多数时间,脚本需要等待特定的事件发生。下面是一些典型的事件等待模式。

使用 page.expect_request(url_or_predicate, **kwargs) 等待拥有指定 URL 的请求:

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 测试

Playwright 可用于访问应用程序的 REST API。

有时,你可能想通过 Python 直接向服务端发送请求,而非加载页面,在其中运行 js 代码。此时,Playwright 可以派上用场的一些示例如下:

  • 测试服务端 API

  • 在访问 Web 应用程序之前,准备服务端状态

  • 在浏览器中运行一些操作后,验证服务端的后置条件

这些都可以通过 APIRequestContext 方法实现。

下面的示例依赖 pytest-playwright 包,它向 Pytest test-runner 添加 Playwright fixture。

  • Writing API Test

  • Prepare server state via API calls

  • Check the server state after running user actions

  • Reuse authentication state

编写 API 测试

APIRequestContext 可以通过网络发送各种 HTTP(S) 请求。下面的示例展示如何使用 Playwright 通过 GitHub API 测试 issue 创建。测试套件做如下事情:
  • 在运行测试之前,创建新仓库
  • 创建一些 issue 及验证服务端状态
  • 在运行测试后删除仓库

配置

Github API 需要认证,因此我们将为所有测试配置一次 token。同时,我们也将设置 baseURL 以简化测试。

import os
from typing import Generator

import pytest
from playwright.sync_api import Playwright, APIRequestContext

GITHUB_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_context
    request_context.dispose()

编写测试

现在我们已初始化请求对象,我们可以添加一些在仓库中创建新 issue 的测试。

import os
from typing import Generator

import pytest
from playwright.sync_api import Playwright, APIRequestContext

GITHUB_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.ok

    issues = api_request_context.get(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues")
    assert issues.ok
    issues_response = issues.json()
    issue = list(filter(lambda issue: issue["title"] == "[Bug] report 1", issues_response))[0]
    assert issue
    assert 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.ok

    issues = api_request_context.get(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues")
    assert issues.ok
    issues_response = issues.json()
    issue = list(filter(lambda issue: issue["title"] == "[Feature] request 1", issues_response))[0]
    assert issue
    assert issue["body"] == "Feature description"

准备(setup)和销毁(teardown)

这些测试假定仓库存在。你可能想在运行测试之前创建新仓库,之后删除它。为此使用 session fixture。yield 之前的部分在所有测试之前执行,之后的部分在所有测试之后执行。

# ...
@pytest.fixture(scope="session", autouse=True)
def create_test_repository(
    api_request_context: APIRequestContext,
) -> Generator[None, None, None]:
    # Before all
    new_repo = api_request_context.post("/user/repos", data={"name": GITHUB_REPO})
    assert new_repo.ok
    yield
    # After all
    deleted_repo = api_request_context.delete(f"/repos/{GITHUB_USER}/{GITHUB_REPO}")
    assert deleted_repo.ok

完成样例测试

这是 API 测试的完整样例:

from enum import auto
import os
from typing import Generator

import pytest
from playwright.sync_api import Playwright, Page, APIRequestContext, expect

GITHUB_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_context
    request_context.dispose()


@pytest.fixture(scope="session", autouse=True)
def create_test_repository(
    api_request_context: APIRequestContext,
) -> Generator[None, None, None]:
    # Before all
    new_repo = api_request_context.post("/user/repos", data={"name": GITHUB_REPO})
    assert new_repo.ok
    yield
    # After all
    deleted_repo = api_request_context.delete(f"/repos/{GITHUB_USER}/{GITHUB_REPO}")
    assert deleted_repo.ok


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.ok

    issues = api_request_context.get(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues")
    assert issues.ok
    issues_response = issues.json()
    issue = list(
        filter(lambda issue: issue["title"] == "[Bug] report 1", issues_response)
    )[0]
    assert issue
    assert 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.ok

    issues = api_request_context.get(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues")
    assert issues.ok
    issues_response = issues.json()
    issue = list(
        filter(lambda issue: issue["title"] == "[Feature] request 1", issues_response)
    )[0]
    assert issue
    assert 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.ok
    create_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']").first
    expect(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.ok
    assert new_issue.json()["title"] == "[Bug] report 1"
    assert new_issue.json()["body"] == "Bug description"

重用认证状态

Web 应用程序使用基于 Cookie 或基于 Token 的认证,认证状态被存储为 Cookies。Playwright 提供 api_request_context.storage_state(**kwargs) 方法,它可用于从已认证上下文中获取存储状态,然后使用该状态创建新上下文。

BrowserContext 和 APIRequestContext 之间可交换存储状态。你可以通过 API 调用登录,然后使用已有的 Cookie 创建新上下文。下面的代码片段从已认证 APIRequestContext 获取状态,然后使用该状态创建新浏览器上下文(BrowserContext)。

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)
# 开源安全工具 # API # 网络安全工具
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录