1. 项目概述:为什么是 Playwright + Pytest?

如果你正在为 Web 应用的回归测试、兼容性测试或者日常的冒烟测试而头疼,手动点点点不仅效率低下,还容易出错,那么构建一个稳定、易维护的自动化测试框架就成了刚需。市面上工具很多,Selenium 是老牌劲旅,Cypress 是后起之秀,但今天我们要聊的组合——Playwright 和 Pytest——正在成为越来越多团队的首选。这不是简单的工具堆砌,而是一套能显著提升测试开发效率和执行稳定性的“黄金搭档”。

Playwright 是微软开源的现代化浏览器自动化库,它最大的魅力在于“稳”。它直接通过 DevTools 协议与 Chromium、Firefox 和 WebKit 内核的浏览器通信,这意味着它的操作指令是浏览器原生支持的,执行速度快,且对动态内容的等待处理非常智能,几乎不需要你手动写 sleep 。而 Pytest 则是 Python 社区公认的最强大、最灵活的测试框架,没有之一。它的夹具(Fixture)系统、参数化测试、丰富的插件生态,让测试用例的组织、数据管理和执行控制变得异常优雅。

把这两者结合起来,Playwright 负责搞定“浏览器里的事”,提供稳定可靠的页面操作能力;Pytest 则负责搞定“测试本身的事”,管理测试生命周期、数据、报告和并行执行。这个组合解决的,正是传统 UI 自动化测试中“脚本脆弱难维护”、“执行不稳定”、“用例管理混乱”三大痛点。无论你是测试开发工程师,还是需要为自己项目添加自动化测试的全栈开发者,掌握这套框架的搭建,都能让你的工作流变得更加高效和可靠。

2. 框架核心设计与架构选型

搭建一个框架,首先得想清楚它要长什么样,以及为什么这么设计。一个健壮的自动化测试框架,绝不仅仅是把几个库拼在一起跑通几个用例那么简单。它需要具备清晰的层次结构、良好的可维护性和可扩展性。

2.1 分层架构:PO 模型是基石

我们采用经典的 页面对象模型(Page Object Model, PO) 。这是 UI 自动化测试的“最佳实践”,核心思想是将页面的元素定位和操作细节封装在单独的类中,测试用例只关心业务逻辑和断言。这样做的好处显而易见:

  1. 高可维护性 :当页面 UI 发生变更时,你只需要修改对应的 Page 类中的元素定位器,所有引用该页面的测试用例都无需改动。
  2. 高可读性 :测试用例读起来就像产品需求文档,例如 login_page.login(“username”, “password”) ,业务意图非常清晰。
  3. 低冗余 :避免了在多个测试用例中重复编写相同的元素定位和基础操作代码。

在我们的框架中,PO 模型会具体化为几个核心目录:

  • pages/ : 存放所有页面类,每个类对应一个 Web 页面或一个主要页面组件。
  • tests/ : 存放所有的测试用例,用例调用 pages 中的方法。
  • conftest.py : Pytest 的核心配置文件,用于定义全局的夹具,比如浏览器实例的创建和销毁。
  • config/ : 存放配置文件,如环境 URL、用户凭证、超时时间等。
  • utils/ : 存放工具函数,如数据生成、文件操作、自定义断言等。
  • reports/ : 存放测试报告(由 Pytest 插件生成)。
  • fixtures/ : 可选,用于存放更复杂的、可重用的 Pytest 夹具。

2.2 工具选型:为什么是它们?

  • Playwright vs Selenium :Selenium 需要通过 WebDriver 与浏览器通信,这是一个额外的代理层,有时会成为性能和稳定性的瓶颈。Playwright 直接与浏览器内核对话,速度更快,并且原生支持自动等待、网络拦截、文件上传/下载等复杂场景,API 设计也更现代。对于新项目,Playwright 是更优的选择。
  • Pytest vs Unittest :Python 自带的 unittest 框架比较传统,写法冗长,夹具功能弱。Pytest 的夹具系统( @pytest.fixture )功能强大且灵活,可以轻松实现测试前置后置操作、数据注入和资源共享。其丰富的断言写法(直接使用 assert )和插件生态(如 pytest-html , pytest-xdist , pytest-rerunfailures )能极大提升开发体验和框架能力。
  • 管理工具:Poetry 或 Pipenv :强烈建议使用 Poetry 或 Pipenv 来管理项目依赖和虚拟环境,而不是直接用 pip 。它们能精确锁定依赖版本,确保所有团队成员和环境下的依赖一致性,避免“在我机器上是好的”这类问题。

注意 :虽然 Playwright 也支持同步 API,但在 Pytest 框架中,我们通常使用其异步 API 以获得最佳性能。这意味着你需要对 Python 的 async/await 语法有基本了解。别担心,它比想象中简单,而且 Playwright 的异步 API 写起来非常直观。

3. 环境搭建与项目初始化

理论说完了,我们开始动手。第一步是把地基打好。

3.1 创建项目并管理依赖

首先,创建一个新的项目目录,并使用 Poetry 初始化(如果你习惯用 Pipenv,操作类似)。

mkdir playwright-pytest-framework
cd playwright-pytest-framework
poetry init -n # 交互式创建 pyproject.toml,-n 跳过交互

编辑生成的 pyproject.toml 文件,添加项目依赖。这是框架稳定性的关键,务必指定版本。

[tool.poetry]
name = "playwright-pytest-framework"
version = "0.1.0"
description = "A robust Web UI automation test framework based on Playwright and Pytest."
authors = ["Your Name <you@example.com>"]

[tool.poetry.dependencies]
python = "^3.8"
playwright = "^1.40.0"
pytest = "^7.4.0"
pytest-asyncio = "^0.21.0"
pytest-html = "^4.0.0"
pytest-xdist = "^3.5.0"
pytest-rerunfailures = "^12.0"
python-dotenv = "^1.0.0"

[tool.poetry.group.dev.dependencies]
# 开发依赖,如代码格式化工具
black = "^23.0"
isort = "^5.12"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

然后安装依赖并安装 Playwright 所需的浏览器:

poetry install
poetry run playwright install chromium # 通常安装 Chromium 即可,它最稳定且兼容性好

pytest-asyncio 插件至关重要,它让 Pytest 能够运行异步测试用例。 pytest-html 用于生成美观的 HTML 报告, pytest-xdist 用于并行测试加速, pytest-rerunfailures 用于失败重试,提升稳定性。 python-dotenv 用于管理环境变量。

3.2 核心配置与夹具设计

接下来创建 conftest.py ,这是 Pytest 的“心脏”,我们将在这里定义最重要的夹具——浏览器和页面上下文。

# conftest.py
import pytest
import asyncio
from playwright.async_api import async_playwright, Browser, BrowserContext, Page
import os
from dotenv import load_dotenv

load_dotenv()  # 加载 .env 文件中的环境变量

@pytest.fixture(scope="session")
def event_loop():
    """为整个测试会话创建一个事件循环。
    这是使用 pytest-asyncio 和 session 作用域 fixture 的推荐做法。"""
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()

@pytest.fixture(scope="session")
async def browser():
    """启动一个浏览器实例,整个测试会话只启动一次。"""
    playwright = await async_playwright().start()
    # 这里可以配置启动参数,如无头模式、窗口大小等
    # headless=False 可以打开浏览器界面,便于调试
    browser = await playwright.chromium.launch(headless=True, args=["--disable-blink-features=AutomationControlled"])
    yield browser
    await browser.close()
    await playwright.stop()

@pytest.fixture
async def context(browser):
    """为每个测试用例创建一个独立的浏览器上下文。
    上下文相当于一个独立的‘隐身’会话,隔离 cookies、localStorage 等,用例间互不干扰。"""
    # 可以在这里配置上下文选项,如视口大小、忽略 HTTPS 错误、设置用户代理等
    context = await browser.new_context(
        viewport={"width": 1920, "height": 1080},
        ignore_https_errors=True,
        # user_agent=“...” # 可以自定义 UA
    )
    yield context
    await context.close()

@pytest.fixture
async def page(context):
    """为每个测试用例创建一个新的页面(标签页)。这是最常用的 fixture。"""
    page = await context.new_page()
    yield page
    await page.close()

关键点解析 :

  1. 作用域(Scope) : browser fixture 的作用域是 session ,意味着整个测试运行期间只启动一次浏览器,节省了大量时间。 context 和 page 的作用域是 function (默认),每个测试用例都会获得全新的上下文和页面,保证了测试的隔离性。
  2. 上下文(Context)隔离 :这是 Playwright 的一大优势。每个 context 就像是一个全新的浏览器配置文件,彼此完全隔离。这意味着测试用例 A 登录后的状态,绝不会影响到测试用例 B。这比单纯清理 cookies 要彻底和可靠得多。
  3. 异步(Async) :所有 Playwright 操作都是异步的。 pytest-asyncio 插件允许我们以非常自然的方式在测试函数中使用 async def 和 await 。

3.3 配置文件与环境管理

创建 config 目录和 settings.py 文件,集中管理配置。

# config/settings.py
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

# 环境配置,可通过环境变量 `ENV` 切换
ENV = os.getenv(“ENV”, “staging”).lower()

ENV_CONFIGS = {
    “staging”: {
        “base_url”: “https://staging.example.com”,
        “api_url”: “https://api.staging.example.com”,
        “admin_user”: {“username”: “admin_stag”, “password”: os.getenv(“STAGING_ADMIN_PW”)},
    },
    “production”: {
        “base_url”: “https://www.example.com”,
        “api_url”: “https://api.example.com”,
        “admin_user”: {“username”: “admin_prod”, “password”: os.getenv(“PROD_ADMIN_PW”)},
    },
}

CONFIG = ENV_CONFIGS[ENV]

# 超时时间(毫秒)
TIMEOUT = 30000
# 显式等待超时
EXPECT_TIMEOUT = 10000

# 路径配置
SCREENSHOT_DIR = BASE_DIR / “reports” / “screenshots”
SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)

敏感信息如密码,务必通过环境变量( .env 文件)传入,不要硬编码在代码中。

4. 页面对象(PO)模型的实现

架构和配置就绪,现在开始实现业务层。我们以一个典型的登录页面为例。

4.1 基础页面类封装

首先,在 pages 目录下创建一个 base_page.py ,封装所有页面类的公共操作。

# pages/base_page.py
from playwright.async_api import Page, Locator, expect
from config import settings
import asyncio

class BasePage:
    def __init__(self, page: Page):
        self.page = page
        self.timeout = settings.TIMEOUT

    async def navigate(self, url_suffix=“”):
        """导航到指定页面。"""
        full_url = f“{settings.CONFIG[‘base_url’]}{url_suffix}”
        await self.page.goto(full_url, timeout=self.timeout)
        # 可以在这里添加一些通用的等待条件,比如等待某个核心元素出现
        # await self.page.wait_for_load_state(“networkidle”)

    async def get_element(self, selector: str) -> Locator:
        """获取元素定位器。这里可以进行一些封装,比如自动重试。"""
        return self.page.locator(selector)

    async def click(self, selector: str):
        """点击元素,并加入一些健壮性处理。"""
        element = await self.get_element(selector)
        await element.click(timeout=self.timeout)

    async def fill(self, selector: str, text: str):
        """填充文本框。"""
        element = await self.get_element(selector)
        await element.fill(text, timeout=self.timeout)

    async def get_text(self, selector: str) -> str:
        """获取元素文本。"""
        element = await self.get_element(selector)
        return await element.text_content(timeout=self.timeout)

    async def wait_for_selector(self, selector: str, state=“visible”, **kwargs):
        """等待元素达到特定状态。"""
        timeout = kwargs.get(“timeout”, self.timeout)
        await self.page.wait_for_selector(selector, state=state, timeout=timeout)

    async def take_screenshot(self, name: str, full_page=False):
        """截图并保存到报告目录。"""
        path = settings.SCREENSHOT_DIR / f“{name}_{int(asyncio.get_event_loop().time())}.png”
        await self.page.screenshot(path=path, full_page=full_page)
        return str(path)  # 返回路径,可用于报告附件

4.2 具体页面类实现

然后,实现具体的登录页面 login_page.py 。

# pages/login_page.py
from .base_page import BasePage
from playwright.async_api import Page, expect

class LoginPage(BasePage):
    # 元素定位器:集中管理,便于维护
    USERNAME_INPUT = “#username”
    PASSWORD_INPUT = “#password”
    LOGIN_BUTTON = “button[type=‘submit’]”
    ERROR_MESSAGE = “.alert-error”

    def __init__(self, page: Page):
        super().__init__(page)

    async def navigate_to_login(self):
        """导航到登录页。假设登录页是 /login"""
        await self.navigate(“/login”)

    async def login(self, username: str, password: str):
        """执行登录操作。"""
        await self.fill(self.USERNAME_INPUT, username)
        await self.fill(self.PASSWORD_INPUT, password)
        await self.click(self.LOGIN_BUTTON)
        # 登录后,可以等待页面跳转或某个登录后元素出现
        # await self.page.wait_for_url(“**/dashboard”)

    async def get_error_message(self) -> str:
        """获取登录错误提示信息。"""
        # 使用 Playwright 的 expect 进行等待和断言,更健壮
        error_locator = self.page.locator(self.ERROR_MESSAGE)
        await expect(error_locator).to_be_visible(timeout=5000) # 等待错误信息出现
        return await error_locator.text_content()

实操心得 :在定位器字符串中,优先使用 CSS Selector 或 Playwright 特有的 text= 、 has= 等语义化定位器,它们通常比 XPath 更简洁、性能更好。例如, page.locator(“button:has-text(‘登录’)”) 。将定位器作为类属性集中管理,是 PO 模型的核心纪律。

5. 测试用例编写与 Pytest 高级特性

有了页面对象,编写测试用例就变得非常清晰和简单。

5.1 基础测试用例

在 tests 目录下创建 test_login.py 。

# tests/test_login.py
import pytest
from pages.login_page import LoginPage
from config import settings

class TestLogin:
    """登录功能测试集。"""

    @pytest.mark.asyncio
    async def test_successful_login(self, page):
        """测试成功登录。"""
        login_page = LoginPage(page)
        await login_page.navigate_to_login()
        await login_page.login(
            settings.CONFIG[‘admin_user’][‘username’],
            settings.CONFIG[‘admin_user’][‘password’]
        )
        # 断言:登录后应跳转到仪表盘页面,或出现用户菜单
        await expect(page).to_have_url(“**/dashboard”)
        # 或者断言某个登录后特有的元素存在
        await expect(page.locator(“#user-menu”)).to_be_visible()

    @pytest.mark.asyncio
    @pytest.mark.parametrize(“username, password, expected_error”, [
        (“wrong_user”, “admin123”, “用户名或密码错误”),
        (“admin”, “wrong_pass”, “用户名或密码错误”),
        (“”, “admin123”, “用户名不能为空”),
        (“admin”, “”, “密码不能为空”),
    ])
    async def test_login_failure(self, page, username, password, expected_error):
        """参数化测试:多种失败场景。"""
        login_page = LoginPage(page)
        await login_page.navigate_to_login()
        await login_page.login(username, password)
        # 断言错误信息符合预期
        actual_error = await login_page.get_error_message()
        assert expected_error in actual_error, f“期望错误信息包含 ‘{expected_error}’, 实际得到 ‘{actual_error}’”

5.2 使用夹具进行测试准备和清理

假设很多测试用例都需要先登录。我们可以创建一个 autologin 夹具,放在 conftest.py 或单独的 fixtures 模块中。

# conftest.py (追加)
@pytest.fixture
async def autologin_page(page):
    """返回一个已登录状态的页面对象(例如 DashboardPage)。
    这是一个‘工厂模式’夹具,返回的是具体的页面对象,而非 Page 实例。"""
    from pages.login_page import LoginPage
    from pages.dashboard_page import DashboardPage # 假设有仪表盘页面

    login_page = LoginPage(page)
    await login_page.navigate_to_login()
    await login_page.login(
        settings.CONFIG[‘admin_user’][‘username’],
        settings.CONFIG[‘admin_user’][‘password’]
    )
    # 验证登录成功
    await expect(page).to_have_url(“**/dashboard”, timeout=10000)
    # 返回登录后的页面对象
    dashboard_page = DashboardPage(page)
    # 可以在这里做一些登录后的通用等待或操作
    # await dashboard_page.wait_for_loading_complete()
    return dashboard_page

# 在测试用例中使用
# tests/test_dashboard.py
class TestDashboard:
    @pytest.mark.asyncio
    async def test_welcome_message(self, autologin_page): # 直接注入已登录的页面
        dashboard_page = autologin_page
        welcome_text = await dashboard_page.get_welcome_text()
        assert “欢迎回来” in welcome_text

5.3 钩子函数与报告增强

Pytest 的钩子函数(Hook)非常强大,可以让我们在测试生命周期的各个阶段插入自定义逻辑。最常用的是在测试失败时自动截图。

# conftest.py (追加)
import pytest
from datetime import datetime

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """在测试用例生成报告时,如果测试失败,自动截图。"""
    outcome = yield
    report = outcome.get_result()
    if report.when == “call” and report.failed:
        # 获取测试用例中的 page fixture
        page = item.funcargs.get(“page”)
        if page:
            # 确保 page 是有效的,并且是 Playwright 的 Page 对象
            from playwright.async_api import Page
            if isinstance(page, Page):
                # 注意:钩子函数是同步的,但 page.screenshot 是异步的。
                # 我们需要在异步上下文中运行它。这里使用 asyncio.run 的变体。
                # 更安全的方式是使用 item.funcargs 中的 event_loop
                loop = item.funcargs.get(“_asyncio_loop”)
                if loop and loop.is_running():
                    import asyncio
                    async def take_screenshot():
                        screenshot_path = f“reports/screenshots/{item.name}_{datetime.now().strftime(‘%Y%m%d_%H%M%S’)}.png”
                        await page.screenshot(path=screenshot_path, full_page=True)
                        # 将截图路径附加到测试报告中
                        if hasattr(report, “extra”):
                            from pytest_html import extras
                            report.extras.append(extras.png(screenshot_path))
                    # 在当前事件循环中创建任务
                    asyncio.create_task(take_screenshot())

注意 :在钩子函数中处理异步操作需要格外小心,因为 Pytest 的钩子本身是同步的。上面的示例是一种简化处理,在实际复杂场景中,可能需要更精细的异步任务管理,或者使用 pytest-html 插件提供的 pytest_html_results_table_row 钩子来添加附件。更稳健的做法是在测试用例内部,通过 try...except 在断言失败时直接调用页面对象的截图方法。

6. 测试执行、报告与持续集成

框架写好了,用例也写了,最后一步是如何高效地运行它并获取结果。

6.1 使用 Pytest 命令行执行测试

Pytest 提供了极其丰富的命令行选项。

# 在项目根目录下执行
# 1. 运行所有测试
poetry run pytest

# 2. 运行特定目录或文件
poetry run pytest tests/
poetry run pytest tests/test_login.py

# 3. 运行标记的测试 (例如标记为‘smoke’的冒烟测试)
# 先在测试函数上添加装饰器 @pytest.mark.smoke
poetry run pytest -m smoke

# 4. 并行执行测试,大幅缩短执行时间 (使用 pytest-xdist)
poetry run pytest -n auto  # ‘auto’ 表示使用与 CPU 核心数相同的 worker

# 5. 失败重试,应对前端渲染或网络不稳定导致的偶发失败 (使用 pytest-rerunfailures)
poetry run pytest --reruns 3 --reruns-delay 2  # 失败后重试3次,每次间隔2秒

# 6. 生成 HTML 报告
poetry run pytest --html=reports/report.html --self-contained-html

# 7. 组合使用:并行、重试并生成报告
poetry run pytest -n auto --reruns 2 --html=reports/report.html -v

6.2 配置 pytest.ini 文件

将常用的命令行选项固化到 pytest.ini 配置文件中,让执行更简便。

# pytest.ini
[pytest]
asyncio_mode = auto
addopts =
    -v
    --strict-markers
    --html=reports/report.html
    --self-contained-html
    --reruns 2
    --reruns-delay 1
    -n auto
markers =
    smoke: 冒烟测试用例
    regression: 回归测试用例
    slow: 执行较慢的测试用例
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*

配置了 asyncio_mode = auto ,Pytest 会自动识别异步测试函数。 addopts 定义了默认的附加选项,这样只需运行 poetry run pytest 就能启用所有优化功能。

6.3 集成到 CI/CD 流水线

自动化测试的价值在持续集成(CI)中才能最大化体现。以下是一个 GitHub Actions 工作流的示例片段:

# .github/workflows/test.yml
name: UI Automation Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: ‘3.10’
      - name: Install Poetry
        run: pipx install poetry
      - name: Install dependencies
        run: poetry install --no-interaction
      - name: Install Playwright Browsers
        run: poetry run playwright install chromium --with-deps
      - name: Run tests
        run: poetry run pytest
        env:
          ENV: “staging” # 设置测试环境
          STAGING_ADMIN_PW: ${{ secrets.STAGING_ADMIN_PW }}
      - name: Upload test report
        if: always() # 无论测试成功与否都上传报告
        uses: actions/upload-artifact@v3
        with:
          name: playwright-test-report
          path: reports/

7. 常见问题排查与性能优化

在实际使用中,你肯定会遇到各种问题。这里记录一些典型的“坑”和解决方案。

7.1 元素定位失败:等待策略

这是 UI 自动化中最常见的问题。Playwright 内置了智能等待,但有时仍需显式控制。

  • 问题 : page.click(“button”) 失败了,日志显示 TimeoutError 。
  • 排查 :
    1. 元素未加载 :使用 page.wait_for_selector(“button”, state=“visible”) 或 page.locator(“button”).wait_for(state=“visible”) 确保元素可见。
    2. 元素被遮挡 :使用 page.click(“button”, force=True) 强制点击(慎用),或检查是否有弹窗、遮罩层。
    3. 定位器不稳定 :避免使用绝对 XPath 或依赖动态生成的类名/ID。优先使用稳定的属性,如 data-testid (需要前端配合添加)。使用 Playwright 的录制工具 ( playwright codegen ) 生成定位器是一个很好的起点,但需要人工审查和优化。
    4. 页面在 iframe 内 :需要先切换到 iframe: frame = page.frame(name=“iframe-name”) ,然后 frame.click(“button”) 。

7.2 测试执行慢:并行与优化

  • 启用并行 :务必使用 pytest-xdist ( -n auto )。
  • 复用浏览器 :我们已经通过 session 作用域的 browser fixture 实现了。
  • 减少不必要的导航 :如果一组测试用例都在同一个应用模块内,考虑使用 autologin_page 这样的夹具,避免每个用例都从头登录。
  • 禁用非必要资源 :在创建浏览器上下文时,可以拦截不必要的请求(如图片、样式表)以加速加载。
    async def context(browser):
        context = await browser.new_context()
        # 拦截并中止图片请求
        await context.route(“**/*.{png,jpg,jpeg}”, lambda route: route.abort())
        yield context
    

    注意 :这可能会影响页面渲染,仅在对纯功能测试且不关心UI的测试中建议使用。

7.3 测试偶发性失败:重试与截图

  • 使用重试插件 : pytest-rerunfailures 是处理网络抖动或前端轻微延迟导致失败的首选方案。
  • 失败自动截图 :如前文钩子函数所示,这是调试的黄金标准。一张截图能提供比日志多得多的信息。
  • 增加超时时间 :对于某些特别慢的操作,可以局部增加超时: await page.click(“button”, timeout=60000) 。
  • 使用更稳定的断言 :Playwright 的 expect 断言内置了等待和重试机制,比直接 assert page.text_content() == “xxx” 更稳定。
    # 不稳定的写法
    assert await page.text_content(“h1”) == “Welcome”
    # 稳定的写法
    await expect(page.locator(“h1”)).to_have_text(“Welcome”)
    

7.4 环境与依赖问题

  • 浏览器启动失败 :确保已运行 playwright install 。在 CI 环境中,可能需要安装系统依赖,Playwright 的 CLI 有 --with-deps 选项(如 playwright install chromium --with-deps )或使用官方 Docker 镜像 ( mcr.microsoft.com/playwright/python )。
  • 异步事件循环冲突 :如果遇到 Event loop is closed 或类似错误,请确保正确配置了 event_loop fixture(如本文 conftest.py 所示),并且测试函数标记了 @pytest.mark.asyncio 。

构建一个成熟的自动化测试框架是一个迭代的过程。从最简单的用例开始,逐步完善页面对象、夹具、配置和报告。这套基于 Playwright 和 Pytest 的框架,以其稳定性、可维护性和强大的生态系统,能够为你的 Web 应用提供坚实的自动化测试保障。记住,好的框架不是一蹴而就的,而是在解决一个又一个实际问题的过程中打磨出来的。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐