""" 测试配置文件 提供全局测试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)