第 9 章 · 认证状态复用 storage_state
本章目标:理解 Playwright 的认证复用模型,学会保存与加载
storage_state,按角色管理多份登录态,并在 pytest 中通过 fixture 让全部测试"开机即已登录"。
9.1 认证为什么拖慢 E2E
绝大多数 Web 应用的测试都绕不开登录。如果每个测试都从头走一遍登录表单:每个用例多花 2–5 秒、登录页改版会打挂所有用例、验证码/2FA 更是自动化噩梦。
Playwright 的解法建立在它的隔离模型上:认证状态(cookies、localStorage、IndexedDB)都挂在 BrowserContext 上,而 context 可以把这份状态导出成 JSON 文件、再在任意新 context 中注入还原——这就是 storage_state:
# ① 登录一次,把状态存盘
page.goto("https://github.com/login")
page.get_by_label("Username or email address").fill("username")
page.get_by_label("Password").fill("password")
page.get_by_role("button", name="Sign in").click()
page.wait_for_load_state()
storage = context.storage_state(path="state.json") # 导出为文件
# ② 之后任何 context 都可以"出生即已登录"
authed = browser.new_context(storage_state="state.json")
page2 = authed.new_page()
page2.goto("https://github.com") # 无需再走登录流程安全提醒
官方文档强烈建议:状态文件里包含可冒充你账号的敏感 Cookie 与令牌,务必把存放目录(如 playwright/.auth/)加入 .gitignore,绝不能提交进仓库。
9.2 按角色管理多份认证
权限体系复杂的系统需要"管理员视角""普通用户视角"分别测试。做法就是给每个角色各存一份状态文件:
import os
os.makedirs("playwright/.auth", exist_ok=True)
def login_and_save(browser, creds, out_path):
"""通用登录例程:登录成功后把 storage_state 写到指定文件"""
context = browser.new_context()
page = context.new_page()
page.goto("https://myapp.example/login")
page.get_by_label("用户名").fill(creds["user"])
page.get_by_label("密码").fill(creds["pass"])
page.get_by_role("button", name="登录").click()
expect(page.get_by_text("工作台")).to_be_visible() # 确认真的登录成功
context.storage_state(path=out_path) # 按角色落盘
context.close()
login_and_save(browser, ADMIN_CREDS, "playwright/.auth/admin.json")
login_and_save(browser, USER_CREDS, "playwright/.auth/user.json")
# 各自的测试加载对应的角色
admin_ctx = browser.new_context(storage_state="playwright/.auth/admin.json")
user_ctx = browser.new_context(storage_state="playwright/.auth/user.json")9.3 在 pytest 中用 fixture 加载登录态
结合 pytest-playwright 提供的 browser fixture,写一个 auth_page fixture,让依赖它的测试自动获得已登录的页面:
# conftest.py
import pytest
from playwright.sync_api import expect, Page
@pytest.fixture(scope="session") # 会话级:整场测试只登录一次
def auth_context(browser, base_url):
context = browser.new_context(
storage_state="playwright/.auth/user.json",
base_url=base_url,
)
yield context
context.close()
@pytest.fixture
def auth_page(auth_context) -> Page:
page = auth_context.new_page()
yield page
@pytest.fixture(scope="session", autouse=False)
def ensure_auth_state(browser, base_url):
"""状态文件不存在时现场生成一份(首次运行 / CI 冷启动兜底)"""
import os
path = "playwright/.auth/user.json"
if not os.path.exists(path):
ctx = browser.new_context(base_url=base_url)
page = ctx.new_page()
page.goto("/login")
page.get_by_label("用户名").fill(os.environ["E2E_USER"])
page.get_by_label("密码").fill(os.environ["E2E_PASS"])
page.get_by_role("button", name="登录").click()
expect(page.get_by_text("工作台")).to_be_visible()
ctx.storage_state(path=path)
ctx.close()注意作用域选择:auth_context 用 scope="session" 复用同一个 context;如果某些测试会破坏会话状态(登出、改资料),就把它降级为函数级或单独提供干净的 fixture 变体。
9.4 Token 过期的处理策略
storage_state 只是快照,token 过期后所有用例会在同一时刻集体变红。常用策略:
- 短周期内有效 + CI 前置刷新:CI 每次构建先跑一个"登录并导出 state"的 setup 步骤,保证整轮测试期间 token 新鲜;
- 检测失效自动重建:fixture 里访问首页后检查是否被踢回登录页,是则重新执行登录例程并覆盖 state 文件:
def is_logged_in(page) -> bool:
"""访问受保护页面,判断是否被重定向回登录页"""
page.goto("/dashboard")
page.wait_for_load_state()
return "login" not in page.url
# 在加载状态的 fixture 里做一次体检,失效则重建
if not is_logged_in(auth_page):
login_and_save(browser, USER_CREDS, "playwright/.auth/user.json")- 延长测试环境 token 有效期:专供 E2E 的环境把过期时间调长到小时级,是最简单可靠的工程折衷;
- 官方推荐的完整形态是 setup 项目:用一个专门的项目跑登录脚本产出状态文件,其余项目声明依赖它(Node 版 Playwright 的 project dependencies 特性;Python 侧等价做法即上面的 session 级 fixture 兜底)。
本章小结
- 认证状态属于 BrowserContext,
storage_state(path=...)导出、new_context(storage_state=...)注入; - 状态文件含敏感凭据,必须 gitignore;
- 多角色 = 多份状态文件,命名按角色组织;
- pytest 中用 session 级 fixture 加载登录态,配合"不存在则现场登录"的兜底逻辑应对 CI 冷启动与 token 过期。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. context.storage_state(path="state.json") 导出的内容包含哪些认证数据?
2. 关于认证状态文件的仓库安全,官方的建议是?
3. pytest 中让"已登录页面"供多个测试复用的最佳作用域选择是?
4. storage_state 文件里的 token 过期后,下列哪种处理方式不可取?
🛠️ 动手实践
- 为任一支持本地注册的演示站点实现"注册→登录→导出 storage_state→新 context 直接进入个人中心"的完整脚本。
- 把本章的
ensure_auth_state兜底 fixture 补全到你的 conftest.py,删除 state 文件后验证测试仍能整体跑通。 - 为 admin/user 两个角色各建一份登录态,编写两个测试分别断言"管理员能看到后台入口""普通用户看不到"。