f14002559e
refactor(components): 调整头部和页脚布局样式 style(hero-section): 更新徽章动画效果 docs: 添加测试框架README文档 test: 实现首页、导航和联系表单的测试用例 ci: 添加CI测试脚本和配置
344 lines
9.8 KiB
Python
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)
|