Featured image of post 用Playwright Fixture支持多用户和不同上下文场景

用Playwright Fixture支持多用户和不同上下文场景

介绍如何创建一个自定义的 Fixture 来处理多用户的交互场景

在现代 Web 开发中,应用程序通常需要处理多个用户同时交互的场景。无论是实时协作工具、社交媒体平台,还是电子商务网站,测试这些多用户交互对于确保应用的稳定性和正确性至关重要。

然而,默认情况下,Playwright 测试是为单个用户设计的,每个测试都在一个隔离的浏览器上下文中运行。

本文我们将介绍如何创建一个自定义的 Fixture 来处理这种多用户的交互场景。

单用户测试的局限性

当我们编写一个标准的 Playwright 测试时,通常会得到一个 page 对象,它代表一个单一的浏览器标签页,并且这个标签页属于一个独立的 browserContext。这个上下文是完全隔离的,拥有自己的 Cookie、本地存储等。这对于大部分需要测试的场景来说,是非常有用的特性。

但是,当我们要测试类似以下的场景时,问题就出现了:

  • 用户 A 发送一个好友请求给用户 B。
  • 用户 B 接受这个好友请求。
  • 用户 A 和用户 B 现在可以看到对方在好友列表中。

在单个测试中,无法同时以用户 A 和用户 B 的身份进行操作,因为此时只有一个 page 对象。我们可能也会手动创建一个新的浏览器上下文来单独处理,但这会让测试代码变得冗长、重复且难以维护。

定制多用户 Fixture

Playwright 中提供了一个强大的功能叫做 “fixtures”,它允许自定义测试的执行环境。我们可以利用这个功能来创建一个 fixture,它这样就能为每个测试提供一个或多个完全独立的用户环境。

利用 Python 的 pytest-playwright 库,我们通过 @pytest.fixture 装饰器来实现。

下面我们将创建两个 fixture:user_oneuser_two。每个 fixture 都会负责创建自己的 BrowserContextPage,并在测试结束后自动清理它们。

代码实现

这里我们将使用 pytestplaywright 的异步 API,因为这更接近实际的异步场景。

首先,确保安装了必要的库:

1
2
pip install pytest pytest-playwright
playwright install

现在,在测试文件conftest.py 中,定义以下 fixture:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# conftest.py
import pytest
from playwright.async_api import Page, BrowserContext, Browser, expect

@pytest.fixture
async def user_one(browser: Browser):
    """
    为用户一提供一个独立的浏览器上下文和页面。
    """
    # 创建一个全新的、隔离的浏览器上下文
    context = await browser.new_context()
    # 在该上下文中创建一个新页面
    page = await context.new_page()
    
    # 将上下文和页面作为一个字典提供给测试函数
    yield {"context": context, "page": page}
    
    # 测试结束后,关闭上下文以清理资源
    await context.close()

@pytest.fixture
async def user_two(browser: Browser):
    """
    为用户二提供一个独立的浏览器上下文和页面。
    """
    context = await browser.new_context()
    page = await context.new_page()
    
    yield {"context": context, "page": page}
    
    await context.close()

代码解释

  • 定义了两个异步 fixture:user_oneuser_two
  • 每个 fixture 都依赖于 pytest-playwright 自动提供的 browser fixture。
  • yield 关键字是 pytest fixture 的核心。yield 之前的代码是设置部分,yield 的值会被传递给测试函数,yield 之后的代码是拆卸部分。
  • 这样,每个使用这些 fixture 的测试都会获得两个完全独立的用户环境。

实际使用

现在,我们可以在测试中轻松地使用 user_oneuser_two fixture 了。只需将它们作为参数添加到测试函数中即可。

假设我们有一个简单的聊天应用,我们想测试用户 A 发送消息,用户 B 能否收到。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# test_multi_user_chat.py
import pytest
from playwright.async_api import expect

# 注意:user_one 和 user_two 的 fixture 定义可以放在 conftest.py 中,
# pytest 会自动发现它们。这里为了方便展示,我们假设它们已经可用。

@pytest.mark.asyncio
async def test_chat_between_two_users(user_one, user_two):
    # 获取两个用户的页面对象
    page_a = user_one['page']
    page_b = user_two['page']

    # 用户 A 进入聊天室
    await page_a.goto("https://your-chat-app.com/room/123")
    await page_a.get_by_label("Username").fill("UserA")
    await page_a.get_by_role("button", name="Join").click()

    # 用户 B 进入同一个聊天室
    await page_b.goto("https://your-chat-app.com/room/123")
    await page_b.get_by_label("Username").fill("UserB")
    await page_b.get_by_role("button", name="Join").click()

    # 用户 A 发送一条消息
    message_text = "Hello from User A!"
    await page_a.get_by_label("Message").fill(message_text)
    await page_a.get_by_role("button", name="Send").click()

    # 用户 B 验证是否收到了消息
    # 我们需要等待消息出现在用户 B 的页面上
    await expect(page_b.get_by_text(message_text)).to_be_visible()

    # 用户 B 回复消息
    reply_text = "Hello from User B!"
    await page_b.get_by_label("Message").fill(reply_text)
    await page_b.get_by_role("button", name="Send").click()

    # 用户 A 验证是否收到了回复
    await expect(page_a.get_by_text(reply_text)).to_be_visible()

登录状态保持

在实际测试中,通常会希望用户在开始测试前就已经处于登录状态。这里可以通过 storageState 选项来实现这一点。只需在创建上下文时加载一个包含认证信息的 JSON 文件即可。

代码类似:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import asyncio
from playwright.async_api import async_playwright, Browser

async def generate_user_state(
    browser: Browser,
    base_url: str,
    user_email: str,
    user_password: str,
    output_path: str = "user_one_state.json",
):
    """
    生成用户认证状态文件(如 user_one_state.json)
    
    :param browser: Playwright Browser 实例
    :param base_url: 应用基础 URL
    :param user_email: 用户邮箱
    :param user_password: 用户密码
    :param output_path: 状态文件输出路径
    """
    # 1. 创建临时浏览器上下文(无状态)
    context = await browser.new_context()
    page = await context.new_page()

    try:
        # 2. 导航到登录页面并登录
        await page.goto(f"{base_url}/login")
        await page.fill("input[name='email']", user_email)
        await page.fill("input[name='password']", user_password)
        await page.click("button[type='submit']")
        
        # 等待登录成功(例如跳转到仪表盘)
        await page.wait_for_url(f"{base_url}/dashboard", timeout=10000)

        # 3. 保存认证状态到文件
        await context.storage_state(path=output_path)
        print(f"✅ 用户状态已保存到: {output_path}")

    except Exception as e:
        print(f"❌ 登录失败: {e}")
        raise
    finally:
        # 4. 关闭上下文
        await context.close()

# 使用示例
async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)  # 可设为 False 以调试
        await generate_user_state(
            browser,
            base_url="https://your-app.com",
            user_email="user_one@example.com",
            user_password="your_password",
            output_path="user_one_state.json",
        )
        await browser.close()

if __name__ == "__main__":
    asyncio.run(main())

上面的代码,首先我们创建了临时浏览器上下文,并执行了登录流程,然后利用storageState存储到json文件后,关闭上下文,释放资源。

得到Json文件形如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
  "cookies": [
    {
      "name": "session_id",
      "value": "abc123",
      "domain": "your-app.com",
      "path": "/",
      "expires": -1,
      "httpOnly": true,
      "secure": true,
      "sameSite": "Lax"
    }
  ],
  "origins": [
    {
      "origin": "https://your-app.com",
      "localStorage": [
        {
          "name": "token",
          "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
        }
      ]
    }
  ]
}

然后我们修改对应的 user_one fixture:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# conftest.py (更新后的 user_one fixture)

@pytest.fixture
async def user_one(browser: Browser):
    """
    为用户一提供一个独立的、已登录的浏览器上下文和页面。
    """
    # 假设我们已经通过一个单独的脚本或测试生成了 'user_one_state.json'
    context = await browser.new_context(storage_state="user_one_state.json")
    page = await context.new_page()
    
    yield {"context": context, "page": page}
    
    await context.close()

这样,每次使用 user_one fixture 的测试都会自动获得一个已经登录的用户环境,极大地简化了测试的设置过程。

总结

通过创建自定义的多用户 fixture,我们可以轻松地扩展 Playwright 测试的能力,以应对复杂的多用户交互场景。

使用 Hugo 构建
主题 StackJimmy 设计