在现代 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_one 和 user_two。每个 fixture 都会负责创建自己的 BrowserContext 和 Page,并在测试结束后自动清理它们。
代码实现
这里我们将使用 pytest 和 playwright 的异步 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_one 和 user_two。 - 每个 fixture 都依赖于
pytest-playwright 自动提供的 browser fixture。 yield 关键字是 pytest fixture 的核心。yield 之前的代码是设置部分,yield 的值会被传递给测试函数,yield 之后的代码是拆卸部分。- 这样,每个使用这些 fixture 的测试都会获得两个完全独立的用户环境。
实际使用
现在,我们可以在测试中轻松地使用 user_one 和 user_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 测试的能力,以应对复杂的多用户交互场景。