如何实现PyTest加载测试文件前执行单次Playwright登录认证,生成/更新有效存储状态文件?
如何实现PyTest加载测试文件前执行单次Playwright登录认证,生成/更新有效存储状态文件?
我完全懂你遇到的这个痛点——用Playwright+PyTest做自动化时,要么本地的认证状态过期,要么新拉代码的同事跑测试直接卡在登录页,之前写的session级fixture还导致每个测试都弹登录窗口,太闹心了!
核心问题其实是:你需要在所有测试启动的最开始,只执行一次认证检查和登录(不管是状态文件不存在还是过期),而且这个过程要在PyTest初始化其他fixture、扫描测试用例之前完成。我结合你的代码调整了方案,绝对能解决你的问题:
第一步:整理基础配置与登录逻辑
先把常量和登录函数抽出来,确保登录时生成的状态文件路径正确,并且只在需要时弹出手动登录窗口(支持OTP):
import time from pathlib import Path from playwright.sync_api import sync_playwright, Browser, BrowserContext, Page import pytest # 全局配置 AUTH_FILE = Path(".auth/storage_state.json") # 认证状态过期时间:这里设为24小时,可根据需求调整 AUTH_EXPIRY_SECONDS = 60 * 60 * 24 def authentication_login(): """手动完成登录(非无头模式),并保存认证状态到文件""" # 确保存储目录存在,避免创建文件时报错 AUTH_FILE.parent.mkdir(exist_ok=True) with sync_playwright() as p: # 非无头启动浏览器,方便你手动输入账号、OTP browser = p.chromium.launch(headless=False, slow_mo=500) context = browser.new_context() page = context.new_page() try: page.goto("http://localhost/") print("\n👉 请在弹出的浏览器窗口完成登录(包括OTP验证),完成后按Playwright Inspector的继续按钮或回车!") # 暂停等待手动登录操作 page.pause() # 登录成功后,保存当前会话的认证状态 context.storage_state(path=AUTH_FILE) print(f"✅ 新的认证状态已保存到 {AUTH_FILE}") finally: # 确保资源被正确释放 context.close() browser.close()
第二步:核心会话级Autouse Fixture
这是关键!用session级+autouse=True的fixture,确保它在所有测试、所有其他fixture之前只执行一次:
@pytest.fixture(scope="session", autouse=True) def ensure_valid_authentication(): """会话启动前自动执行:检查认证状态,无效则触发手动登录""" should_trigger_login = False # 检查状态文件是否存在 if not AUTH_FILE.exists(): print("⚠️ 未找到认证状态文件,将启动手动登录流程...") should_trigger_login = True else: # 检查状态文件是否过期 time_since_last_update = time.time() - AUTH_FILE.stat().st_mtime if time_since_last_update > AUTH_EXPIRY_SECONDS: print(f"⚠️ 认证状态已过期(超过{AUTH_EXPIRY_SECONDS//3600}小时),将启动手动登录流程...") should_trigger_login = True if should_trigger_login: authentication_login() # yield 之后的代码会在所有测试结束后执行(这里不需要额外操作,直接yield即可) yield
第三步:调整其他测试Fixtures
把playwright、浏览器上下文等fixture设为session级,避免重复创建资源,同时确保它们使用已生成的有效认证状态:
@pytest.fixture(scope="session") def playwright(): with sync_playwright() as p: yield p @pytest.fixture(scope="session") def browser(playwright): # 这里可以设为headless=True,因为不需要手动操作了 browser = playwright.chromium.launch(headless=True) yield browser browser.close() @pytest.fixture(scope="session") def browser_context(browser): # 从已有的认证状态文件创建上下文 context = browser.new_context(storage_state=AUTH_FILE) yield context context.close() @pytest.fixture(scope="function") def page(browser_context): page = browser_context.new_page() page.goto("http://localhost/") # 可以保留你自己的加载等待逻辑 # wait_for_loaders_to_complete(page) yield page page.close()
为什么这个方案能解决你的问题?
- 执行时机绝对优先:
ensure_valid_authentication是session级+autouse=True,PyTest会在整个测试会话的最开始就执行它,比所有测试用例、其他fixture都早,而且只执行一次。 - 避免重复登录:只有在状态文件不存在/过期时才会弹出手动登录窗口,后续所有测试都会直接复用有效状态,不会再弹登录页。
- 资源高效:浏览器、上下文都是
session级,整个测试会话只会启动一次浏览器,不会像之前那样每个测试都开新窗口。
额外优化建议
- 加文件锁避免并行测试冲突:如果用了pytest-xdist并行跑测试,可以加个文件锁确保只有一个进程触发登录:
from filelock import FileLock def authentication_login(): lock_file = AUTH_FILE.parent / "login.lock" with FileLock(lock_file): # 原来的登录逻辑... - 支持命令行参数调整配置:可以把过期时间、状态文件路径做成pytest命令行参数,方便灵活修改:
def pytest_addoption(parser): parser.addoption("--auth-expiry", type=int, default=60*60*24, help="认证过期时间(秒)") parser.addoption("--auth-file", type=Path, default=AUTH_FILE, help="认证状态文件路径")
内容来源于stack exchange
相关产品推荐
相关产品推荐

