接上篇,首先做了几个优化点:
- config.py中将敏感信息(数据库地址、测试账号等)改为从.env中读取,并将.env加入.gitignore(不提交至git)
- Page Object 里不建议改 sys.path,把项目根目录交给 pytest 管理,在pytest.ini 里加
pythonpath = .
后续的页面都要基于登录态进行,那Playwright 里最适合的是 保存登录态,后续用例直接复用登录后的浏览器状态
介绍一下fixture
Fixture 是 Playwright 中用于管理测试准备和清理工作的核心机制,它能确保每个测试都运行在一个干净、隔离的环境中,并支持代码的高效复用。其核心理念是依赖注入,即在测试执行前自动注入所需资源,执行后再统一清理,从而实现解耦与复用
开始执行
- 创建conftest.py,封装公共 fixture,用于管理登录态和业务页面访问
python
import time
from pathlib import Path
from urllib.parse import urlparse
import pytest
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
import config
from pages.login_page import LoginPage
AUTH_DIR = Path(".auth")
AUTH_STATE_PATH = AUTH_DIR / "state.json"
# 登录态有效期按 23 小时处理,避开后端 24 小时过期边界。
AUTH_EXPIRE_SECONDS = 23 * 60 * 60
def _get_required_test_value(key):
"""从 TEST_CONFIG 中读取必填配置,缺失时直接让用例失败并提示配置项。"""
value = config.TEST_CONFIG.get(key)
if not value:
raise ValueError(f"请通过环境变量配置测试数据: {key}")
return value
def _get_login_success_path():
"""读取登录成功后的目标路径,用于判断登录是否完成。"""
return config.TEST_CONFIG.get("login_success_path") or "/approvalManage"
def _auth_state_is_valid():
"""检查本地登录态文件是否存在,并且没有超过约定的有效期。"""
if not AUTH_STATE_PATH.exists():
return False
return time.time() - AUTH_STATE_PATH.stat().st_mtime < AUTH_EXPIRE_SECONDS
def _auth_state_passes_smoke_check(browser, browser_context_args):
"""使用本地登录态打开登录后页面,确认 token 仍可被前端识别。"""
context = browser.new_context(
**browser_context_args,
storage_state=str(AUTH_STATE_PATH),
)
page = context.new_page()
try:
page.goto(_get_login_success_path())
page.wait_for_load_state("networkidle", timeout=15000)
page.wait_for_timeout(1500)
token = page.evaluate("localStorage.getItem('geoAdminToken')")
return bool(token) and urlparse(page.url).path != "/login"
except Exception:
return False
finally:
context.close()
def _wait_for_path(page, target_path, timeout=15000):
"""等待当前页面 path 变成目标值,只校验路径,不绑定域名。"""
try:
page.wait_for_function(
"([targetPath]) => window.location.pathname === targetPath",
arg=[target_path],
timeout=timeout,
)
except PlaywrightTimeoutError:
current_path = urlparse(page.url).path
raise AssertionError(f"当前路径为 {current_path},预期路径为 {target_path}")
def _create_auth_state(browser, browser_context_args):
"""执行一次真实登录,并把登录后的浏览器状态保存到 .auth/state.json。"""
AUTH_DIR.mkdir(exist_ok=True)
context = browser.new_context(**browser_context_args)
page = context.new_page()
login_page = LoginPage(page)
login_page.goto_page()
login_page.login(
_get_required_test_value("test_login_phone_number"),
_get_required_test_value("test_login_pwd"),
)
login_success_path = _get_login_success_path()
_wait_for_path(page, login_success_path)
context.storage_state(path=str(AUTH_STATE_PATH))
context.close()
@pytest.fixture(scope="session")
def auth_state(browser, browser_context_args):
"""
生成或复用已登录 storage_state。
当本地登录态不存在、超过 23 小时,或冒烟校验失败时,会重新登录生成。
"""
if not _auth_state_is_valid() or not _auth_state_passes_smoke_check(
browser,
browser_context_args,
):
_create_auth_state(browser, browser_context_args)
return str(AUTH_STATE_PATH)
@pytest.fixture()
def auth_page(browser, browser_context_args, auth_state):
"""
创建已登录页面。
后续业务用例优先使用 auth_page,登录页自身测试继续使用默认 page。
"""
context = browser.new_context(
**browser_context_args,
storage_state=auth_state,
)
page = context.new_page()
yield page
context.close()
- 业务页面(需要登录后访问的页面)使用auth_page
默认"page"是pytest-playwright提供的默认页面对象,适合测试未登录场景,例如登录页、忘记密码页。
"auth_page" 是已登录页面 fixture,后续需要登录后访问的业务页面优先使用它,例如以下页面:
python
from pages.my_todo_list_page import MyTodoListPage
def test_my_todo_list_data_display(auth_page):
"""
验证我的待办列表数据正常显示。
"""
my_todo_list_page = MyTodoListPage(auth_page)
response = my_todo_list_page.goto_page()
assert response.ok, "我的待办列表接口请求失败"
response_data = my_todo_list_page.get_response_data(response)
total = response_data.get("total")
data_list = response_data.get("list") or []
assert total is None or total > 0, "我的待办列表接口未返回数据"
assert len(data_list) > 0, "我的待办列表接口 list 为空"
my_todo_list_page.check_pagination_total_visible()
my_todo_list_page.check_list_data_visible()
-
Pytest 发现 test_my_todo_list_data_display需要参数 auth_page。
-
自动寻找并执行同名的 fixture 函数,将其返回值传递给测试。
-
测试结束后,若 fixture 中使用了 yield,则执行清理代码
至此,我们的项目结构又进一步完善,增加了以下几个文件
