Files
novalon-website/e2e-tests/tests/conftest.py
T
张翔 f14002559e feat(e2e-tests): 添加端到端测试框架及测试用例
refactor(components): 调整头部和页脚布局样式
style(hero-section): 更新徽章动画效果

docs: 添加测试框架README文档
test: 实现首页、导航和联系表单的测试用例
ci: 添加CI测试脚本和配置
2026-02-02 19:36:33 +08:00

344 lines
9.8 KiB
Python

"""
测试配置文件
提供全局测试fixture和钩子函数
"""
import os
import sys
import time
from pathlib import Path
from typing import Generator, Optional
import pytest
from pytest import Config
from playwright.sync_api import Browser, BrowserContext, Page, Playwright
from playwright.sync_api import Error as PlaywrightError
# 添加项目根目录到Python路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from config.settings import get_settings
from config.browsers import get_browser_factory, BrowserConfigManager
from utils.logger import get_logger
from utils.report_generator import get_report_manager, TestResult, TestStatus
from utils.data_generator import get_test_data_generator
@pytest.fixture(scope="session")
def settings() -> Generator:
"""获取测试配置"""
yield get_settings()
@pytest.fixture(scope="session")
def logger() -> Generator:
"""获取日志记录器"""
yield get_logger()
@pytest.fixture(scope="session")
def test_data_generator() -> Generator:
"""获取测试数据生成器"""
yield get_test_data_generator()
@pytest.fixture(scope="session")
def browser_factory() -> Generator:
"""获取浏览器工厂"""
factory = get_browser_factory()
yield factory
@pytest.fixture(scope="session")
def browser_context(
browser_factory: BrowserConfigManager,
settings
) -> Generator:
"""创建浏览器上下文"""
browser, context, page = browser_factory.create_browser_session(
browser_name=settings.default_browser,
headless=settings.headless_mode
)
yield context, page
# 清理(在会话结束时)
try:
page.close()
except Exception:
pass
try:
context.close()
except Exception:
pass
try:
browser_factory.close_browser()
except Exception:
pass
@pytest.fixture
def page(browser_context, settings) -> Generator:
"""创建页面"""
context, page = browser_context
# 设置默认超时
page.set_default_timeout(settings.page_load_timeout)
page.set_default_navigation_timeout(settings.page_load_timeout)
yield page
# 截图(如果测试失败)
if hasattr(page, "_test_failed") and page._test_failed:
test_name = getattr(pytest, "_test_name", "unknown")
screenshots_dir = Path(settings.screenshots_dir)
screenshots_dir.mkdir(parents=True, exist_ok=True)
screenshot_path = screenshots_dir / f"{test_name}_failed.png"
try:
page.screenshot(path=str(screenshot_path))
logger = get_logger()
logger.error(f"失败截图已保存: {screenshot_path}")
except Exception as e:
logger = get_logger()
logger.error(f"保存失败截图时出错: {e}")
@pytest.fixture
def home_page(page: Page, settings) -> Generator:
"""创建首页对象"""
from pages.home_page import HomePage
home = HomePage(page, settings.get_base_url())
yield home
@pytest.fixture
def contact_page(page: Page, settings) -> Generator:
"""创建联系页面对象"""
from pages.contact_page import ContactPage
contact = ContactPage(page, settings.get_base_url())
yield contact
@pytest.fixture(scope="session")
def base_url(settings) -> str:
"""获取基础URL"""
return settings.get_base_url()
@pytest.fixture(scope="session")
def test_results() -> Generator:
"""收集测试结果"""
results = []
yield results
@pytest.fixture(scope="session")
def report_manager() -> Generator:
"""获取报告管理器"""
manager = get_report_manager()
yield manager
@pytest.fixture
def track_test_result(report_manager, test_results):
"""跟踪测试结果fixture"""
from datetime import datetime
import pytest
class TestTracker:
def __init__(self):
self.start_time = None
self.current_result = None
def start_track(self, test_name: str, test_class: str = "", test_file: str = ""):
self.start_time = datetime.now()
logger = get_logger()
logger.log_test_start(test_name, test_class=test_class, test_file=test_file)
def end_track(
self,
test_name: str,
status: TestStatus,
test_class: str = "",
test_file: str = ""
):
end_time = datetime.now()
duration = (end_time - self.start_time).total_seconds()
logger = get_logger()
logger.log_test_end(test_name, status.value, duration)
# 创建测试结果
result = TestResult(
test_id=f"{test_file}_{test_name}",
test_name=test_name,
test_file=test_file,
test_class=test_class,
status=status,
start_time=self.start_time,
end_time=end_time,
duration=duration
)
# 添加到报告管理器
report_manager.add_result(result)
test_results.append(result)
return result
tracker = TestTracker()
yield tracker
# Pytest钩子函数
def pytest_configure(config: Config):
"""pytest配置钩子"""
# 设置标记
config.addinivalue_line(
"markers", "smoke: 冒烟测试,快速验证核心功能"
)
config.addinivalue_line(
"markers", "regression: 回归测试,完整功能验证"
)
config.addinivalue_line(
"markers", "performance: 性能测试,页面加载和响应时间"
)
config.addinivalue_line(
"markers", "responsive: 响应式测试,不同屏幕尺寸"
)
config.addinivalue_line(
"markers", "cross_browser: 跨浏览器测试"
)
config.addinivalue_line(
"markers", "form: 表单相关测试"
)
config.addinivalue_line(
"markers", "navigation: 导航测试"
)
config.addinivalue_line(
"markers", "interactive: 用户交互测试"
)
# 创建必要的目录
settings = get_settings()
Path(settings.screenshots_dir).mkdir(parents=True, exist_ok=True)
Path(settings.videos_dir).mkdir(parents=True, exist_ok=True)
Path("reports").mkdir(parents=True, exist_ok=True)
def pytest_sessionstart(session):
"""测试会话开始"""
logger = get_logger()
logger.section("开始E2E测试会话")
logger.info(f"测试会话ID: {session.name}")
logger.info(f"测试数量: {len(session.items)}")
def pytest_sessionfinish(session, exitstatus):
"""测试会话结束"""
logger = get_logger()
logger.section("E2E测试会话结束")
# 生成报告
if exitstatus == 0:
logger.info("✅ 所有测试通过")
else:
logger.warning(f"⚠️ 测试完成,退出码: {exitstatus}")
def pytest_runtest_setup(item):
"""测试运行前设置"""
logger = get_logger()
logger.divider()
logger.log_test_start(item.name)
def pytest_runtest_makereport(item, call):
"""生成测试报告"""
if call.when == "call":
if call.excinfo:
item._test_failed = True
else:
item._test_failed = False
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
"""测试运行钩子"""
yield
# 测试运行后处理
def pytest_collection_modifyitems(config, items):
"""修改测试项目"""
# 按标记排序
marker_priority = {
"smoke": 0,
"performance": 1,
"regression": 2,
"responsive": 3,
"cross_browser": 4,
"form": 5,
"navigation": 6,
"interactive": 7
}
def get_priority(item):
for marker in item.iter_markers():
if marker.name in marker_priority:
return marker_priority[marker.name]
return 10
items.sort(key=get_priority)
# 自定义断言
class E2EAssertions:
"""E2E测试断言类"""
def __init__(self, page: Page):
self.page = page
self.logger = get_logger()
def title_contains(self, expected: str, message: Optional[str] = None) -> None:
"""断言标题包含预期文本"""
actual = self.page.title()
assert expected in actual, message or f"标题不包含 '{expected}': {actual}"
self.logger.log_assertion(f"标题包含 '{expected}'", True)
def url_contains(self, expected: str, message: Optional[str] = None) -> None:
"""断言URL包含预期文本"""
assert expected in self.page.url, message or f"URL不包含 '{expected}': {self.page.url}"
self.logger.log_assertion(f"URL包含 '{expected}'", True)
def element_exists(self, selector: str, timeout: int = 5000) -> None:
"""断言元素存在"""
try:
self.page.wait_for_selector(selector, timeout=timeout)
self.logger.log_assertion(f"元素存在: {selector}", True)
except Exception as e:
self.logger.log_assertion(f"元素存在: {selector}", False)
raise AssertionError(f"元素不存在: {selector}") from e
def element_visible(self, selector: str, timeout: int = 5000) -> None:
"""断言元素可见"""
element = self.page.locator(selector).first
assert element.is_visible(timeout=timeout), f"元素不可见: {selector}"
self.logger.log_assertion(f"元素可见: {selector}", True)
def element_not_visible(self, selector: str, timeout: int = 5000) -> None:
"""断言元素不可见"""
element = self.page.locator(selector).first
assert not element.is_visible(timeout=timeout), f"元素应该不可见: {selector}"
self.logger.log_assertion(f"元素不可见: {selector}", True)
@pytest.fixture
def assert_(page: Page) -> E2EAssertions:
"""断言fixture"""
return E2EAssertions(page)