Selenium 自动化测试(十二):日志报告、依赖注入与框架设计

本篇是系列的收官之作,讲解日志集成、测试报告生成、依赖注入模式,以及如何在 CI/CD 中集成自定义测试框架。

一、日志集成

1.1 使用 Python logging 模块

import logging
import os
from datetime import datetime

class TestLogger:
    """测试日志管理器"""

    def __init__(self, logger_name="selenium_test", log_dir="logs"):
        self.logger = logging.getLogger(logger_name)
        self.logger.setLevel(logging.DEBUG)

        # 避免重复添加 handler
        if self.logger.handlers:
            return

        os.makedirs(log_dir, exist_ok=True)
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

        # 控制台 handler
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.INFO)
        console_format = logging.Formatter(
            "%(asctime)s [%(levelname)s] %(message)s",
            datefmt="%H:%M:%S"
        )
        console_handler.setFormatter(console_format)

        # 文件 handler
        file_handler = logging.FileHandler(
            f"{log_dir}/test_{timestamp}.log",
            encoding="utf-8"
        )
        file_handler.setLevel(logging.DEBUG)
        file_format = logging.Formatter(
            "%(asctime)s [%(levelname)s] %(name)s - %(message)s"
        )
        file_handler.setFormatter(file_format)

        self.logger.addHandler(console_handler)
        self.logger.addHandler(file_handler)

    def info(self, message):
        self.logger.info(message)

    def debug(self, message):
        self.logger.debug(message)

    def warning(self, message):
        self.logger.warning(message)

    def error(self, message):
        self.logger.error(message)

    def step(self, step_name, details=""):
        """记录测试步骤"""
        msg = f"[STEP] {step_name}"
        if details:
            msg += f" - {details}"
        self.logger.info(msg)

    def assertion(self, expected, actual, passed=True):
        """记录断言结果"""
        status = "PASS" if passed else "FAIL"
        self.logger.info(
            f"[ASSERT] {status} - Expected: {expected}, Actual: {actual}"
        )


# 全局 logger 实例
logger = TestLogger()

1.2 Selenium 操作日志拦截

from selenium.webdriver.common.by import By

class LoggedPage(BasePage):
    """带日志记录的 Page 基类"""

    def click(self, locator):
        logger.step("点击元素", str(locator))
        try:
            self.wait.until(EC.element_to_be_clickable(locator)).click()
            logger.debug(f"点击成功: {locator}")
        except Exception as e:
            logger.error(f"点击失败: {locator}, 错误: {e}")
            raise

    def input_text(self, locator, text):
        logger.step("输入文本", f"{locator} = '{text}'")
        try:
            element = self.find_element(locator)
            element.clear()
            element.send_keys(text)
            logger.debug(f"输入成功: {locator}")
        except Exception as e:
            logger.error(f"输入失败: {locator}, 错误: {e}")
            raise

1.3 pytest 日志集成

# conftest.py
import pytest
import logging

def pytest_configure(config):
    """配置 pytest 日志"""
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(message)s",
        datefmt="%H:%M:%S"
    )

@pytest.fixture(autouse=True)
def test_logger(request):
    """为每个测试自动记录开始和结束"""
    logger = logging.getLogger("selenium_test")
    test_name = request.node.name
    logger.info(f"\n{'='*60}")
    logger.info(f"测试开始: {test_name}")
    logger.info(f"{'='*60}")
    yield
    logger.info(f"{'='*60}")
    logger.info(f"测试结束: {test_name}")
    logger.info(f"{'='*60}\n")

二、测试报告

2.1 pytest-html 报告

pip install pytest-html
# 生成 HTML 报告
pytest tests/ --html=report.html --self-contained-html

# 带环境信息
pytest tests/ --html=report.html --self-contained-html \
    --pytesthtml="environment=Staging,platform=macOS"

2.2 Allure 报告(推荐)

# 安装 Allure
pip install allure-pytest

# macOS 安装 Allure 命令行工具
brew install allure

# 运行测试并生成 Allure 数据
pytest tests/ --alluredir=allure-results

# 生成并打开报告
allure serve allure-results

Allure 装饰器

import allure

@allure.feature("用户管理")
@allure.story("登录功能")
@allure.title("使用正确凭据登录系统")
@allure.severity(allure.severity_level.BLOCKER)
def test_login_success(driver):
    """带 Allure 注解的测试"""
    login_page = LoginPage(driver).open()

    with allure.step("输入用户名"):
        login_page.enter_username("admin")

    with allure.step("输入密码"):
        login_page.enter_password("123456")

    with allure.step("点击登录按钮"):
        home_page = login_page.submit()

    with allure.step("验证登录成功"):
        assert home_page.is_logged_in()

    allure.attach(
        driver.get_screenshot_as_png(),
        name="登录成功截图",
        attachment_type=allure.attachment_type.PNG
    )

Allure 常用装饰器

装饰器 说明
@allure.feature 功能模块
@allure.story 用户故事
@allure.title 测试标题
@allure.severity 优先级(BLOCKER/CRITICAL/NORMAL/MINOR/TRIVIAL)
@allure.step 测试步骤
@allure.attach 附加附件(截图、日志等)
@allure.description 测试描述
@allure.tag 标签
@allure.link 关联链接
@allure.issue 关联 Issue
@allure.testcase 关联测试用例

2.3 自定义报告生成器

import json
from datetime import datetime
from pathlib import Path

class TestReportGenerator:
    """自定义测试报告生成器"""

    def __init__(self, output_dir="reports"):
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(exist_ok=True)
        self.results = []

    def add_result(self, test_name, status, duration, error=None, screenshot=None):
        """添加测试结果"""
        self.results.append({
            "name": test_name,
            "status": status,         # pass / fail / skip
            "duration": f"{duration:.2f}s",
            "error": str(error) if error else None,
            "screenshot": screenshot,
            "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        })

    def generate_html(self):
        """生成 HTML 报告"""
        total = len(self.results)
        passed = sum(1 for r in self.results if r["status"] == "pass")
        failed = sum(1 for r in self.results if r["status"] == "fail")
        skipped = sum(1 for r in self.results if r["status"] == "skip")

        html = f"""
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>测试报告 - {datetime.now().strftime('%Y-%m-%d %H:%M')}</title>
    <style>
        body {{ font-family: -apple-system, sans-serif; margin: 40px; background: #f5f5f5; }}
        .summary {{ background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }}
        .stat {{ display: inline-block; padding: 10px 20px; margin-right: 10px; border-radius: 4px; color: white; }}
        .pass {{ background: #22c55e; }}
        .fail {{ background: #ef4444; }}
        .skip {{ background: #eab308; }}
        table {{ width: 100%; border-collapse: collapse; background: white; border-radius: 8px; }}
        th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #e5e7eb; }}
        th {{ background: #f9fafb; font-weight: 600; }}
        .error {{ color: #ef4444; font-size: 12px; }}
    </style>
</head>
<body>
    <h1>Selenium 自动化测试报告</h1>
    <div class="summary">
        <span class="stat pass">通过: {passed}</span>
        <span class="stat fail">失败: {failed}</span>
        <span class="stat skip">跳过: {skipped}</span>
        <span>总计: {total} | 通过率: {passed/total*100:.1f}%</span>
    </div>
    <table>
        <tr><th>用例名称</th><th>状态</th><th>耗时</th><th>时间</th><th>错误信息</th></tr>
"""
        for r in self.results:
            status_class = r["status"]
            html += f"""
        <tr>
            <td>{r['name']}</td>
            <td class="{status_class}">{r['status'].upper()}</td>
            <td>{r['duration']}</td>
            <td>{r['timestamp']}</td>
            <td class="error">{r['error'] or ''}</td>
        </tr>"""
        html += "</table></body></html>"

        report_path = self.output_dir / f"report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
        report_path.write_text(html, encoding="utf-8")
        return str(report_path)

三、依赖注入

3.1 为什么需要依赖注入?

在测试框架中,很多对象需要在多个地方共享和传递:driver、page 对象、配置、日志等。依赖注入(DI)可以统一管理这些对象的生命周期。

3.2 手动依赖注入

class TestContext:
    """测试上下文:管理所有共享对象"""

    def __init__(self, config):
        self.config = config
        self._driver = None
        self._logger = None
        self._pages = {}

    @property
    def driver(self):
        if self._driver is None:
            self._driver = self._create_driver()
        return self._driver

    @property
    def logger(self):
        if self._logger is None:
            self._logger = TestLogger()
        return self._logger

    def get_page(self, page_class):
        if page_class not in self._pages:
            self._pages[page_class] = page_class(self.driver)
        return self._pages[page_class]

    def _create_driver(self):
        options = webdriver.ChromeOptions()
        if self.config.get("headless"):
            options.add_argument("--headless")
        if self.config.get("window_size"):
            w, h = self.config["window_size"]
            options.add_argument(f"--window-size={w},{h}")
        driver = webdriver.Chrome(options=options)
        driver.implicitly_wait(self.config.get("implicit_wait", 5))
        return driver

    def cleanup(self):
        if self._driver:
            self._driver.quit()
            self._driver = None

3.3 pytest fixture 实现依赖注入

# conftest.py
import pytest
import yaml

@pytest.fixture(scope="session")
def config():
    """加载配置"""
    with open("config.yaml", "r", encoding="utf-8") as f:
        return yaml.safe_load(f)

@pytest.fixture(scope="session")
def logger():
    """全局 logger"""
    return TestLogger()

@pytest.fixture(scope="function")
def driver(config):
    """每个测试函数创建新的 driver"""
    options = webdriver.ChromeOptions()
    if config.get("headless", False):
        options.add_argument("--headless")
    driver = webdriver.Chrome(options=options)
    driver.implicitly_wait(config.get("implicit_wait", 5))
    yield driver
    driver.quit()

@pytest.fixture
def login_page(driver):
    """注入登录页面对象"""
    return LoginPage(driver)

@pytest.fixture
def home_page(driver):
    """注入主页面对象"""
    return HomePage(driver)

# 测试中使用
def test_login(driver, login_page, home_page, logger):
    logger.step("打开登录页面")
    login_page.open()
    logger.step("执行登录")
    login_page.login("admin", "123456")
    assert home_page.is_logged_in()

3.4 使用 dependency-injector 库

pip install dependency-injector
from dependency_injector import containers, providers, resources

class Container(containers.DeclarativeContainer):
    """依赖注入容器"""

    config = providers.Configuration()

    # Driver 资源管理
    driver = resources.Resource(
        create_driver,
        config=config,
    )

    # Page 对象工厂
    login_page = providers.Factory(
        LoginPage,
        driver=driver,
    )

    home_page = providers.Factory(
        HomePage,
        driver=driver,
    )

    # 工具类
    logger = providers.Singleton(TestLogger)


def create_driver(config):
    """创建 driver(Resource 工厂函数)"""
    options = webdriver.ChromeOptions()
    if config.headless.get():
        options.add_argument("--headless")
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()


# 使用
container = Container()
container.config.from_yaml("config.yaml")

with container.driver() as driver:
    login = container.login_page()
    login.open().login("admin", "123456")

四、CI/CD 中的框架集成

4.1 完整的 CI/CD 流程

代码提交 → 单元测试 → Selenium 测试 → 报告生成 → 通知

4.2 GitHub Actions 完整配置

name: Selenium Test Suite

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  selenium-tests:
    runs-on: ubuntu-latest

    services:
      chrome:
        image: selenium/standalone-chrome:4.18
        ports:
          - 4444:4444

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install pytest allure-pytest

      - name: Run Selenium tests
        env:
          SELENIUM_REMOTE_URL: http://localhost:4444/wd/hub
          HEADLESS: "true"
        run: |
          pytest tests/ \
            --alluredir=allure-results \
            -v

      - name: Generate Allure report
        if: always()
        run: allure generate allure-results -o allure-report --clean

      - name: Upload Allure report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: allure-report
          path: allure-report/

      - name: Test notification
        if: failure()
        run: |
          echo "测试失败!请检查报告。"

4.3 测试结果通知

import requests

class TestNotifier:
    """测试结果通知"""

    @staticmethod
    def send_to_webhook(webhook_url, results):
        """发送到 Webhook(钉钉/飞书/企业微信)"""
        total = len(results)
        passed = sum(1 for r in results if r["status"] == "passed")
        failed = total - passed

        message = (
            f"Selenium 测试报告\n"
            f"总计: {total} | 通过: {passed} | 失败: {failed}\n"
            f"通过率: {passed/total*100:.1f}%"
        )

        if failed > 0:
            failed_cases = [r for r in results if r["status"] == "failed"]
            message += "\n\n失败用例:\n"
            for case in failed_cases:
                message += f"- {case['name']}: {case['error']}\n"

        payload = {
            "msgtype": "text",
            "text": {"content": message}
        }
        requests.post(webhook_url, json=payload)

    @staticmethod
    def send_slack(webhook_url, results):
        """发送到 Slack"""
        total = len(results)
        passed = sum(1 for r in results if r["status"] == "passed")
        color = "#22c55e" if passed == total else "#ef4444"

        payload = {
            "attachments": [{
                "color": color,
                "title": "Selenium 测试结果",
                "text": f"通过: {passed}/{total} ({passed/total*100:.0f}%)",
                "fields": [
                    {"title": "环境", "value": os.getenv("ENV", "Staging"), "short": True},
                    {"title": "耗时", "value": f"{sum(r['duration'] for r in results):.1f}s", "short": True}
                ]
            }]
        }
        requests.post(webhook_url, json=payload)

五、完整框架项目结构

selenium-framework/
├── config/
│   ├── config.yaml              # 全局配置
│   ├── dev.yaml                 # 开发环境配置
│   └── ci.yaml                  # CI 环境配置
├── pages/                       # Page Object
│   ├── base_page.py
│   ├── login_page.py
│   ├── home_page.py
│   └── components/
│       ├── navigation.py
│       └── sidebar.py
├── tests/                       # 测试用例
│   ├── conftest.py
│   ├── test_login.py
│   ├── test_search.py
│   └── test_dashboard.py
├── data/                        # 测试数据
│   ├── login.json
│   └── search.csv
├── utils/                       # 工具类
│   ├── logger.py
│   ├── wait_helper.py
│   ├── debug_helper.py
│   ├── data_driver.py
│   └── notifier.py
├── reports/                     # 测试报告
├── logs/                        # 日志
├── screenshots/                 # 截图
├── requirements.txt
├── pytest.ini
└── README.md

六、系列总结

本系列 12 篇文章从基础到框架设计,完整覆盖了 Selenium 自动化测试的知识体系:

篇章 主题
01 Selenium 简介、环境搭建与架构原理
02 第一个测试脚本与元素定位策略
03 常用 Selenium API 详解
04 高级元素定位技巧
05 等待机制与脚本优化
06 处理特殊情况(弹窗、多窗口、iframe)
07 Selenium 高级特性(JS 执行、手势、日志)
08 并行测试与 CI/CD 集成
09 问题排查与性能测试
10 Page Object Model 设计模式
11 数据驱动测试
12 日志报告、依赖注入与框架设计

掌握这些知识,你已经具备了构建企业级 Selenium 自动化测试框架的完整能力。

继续学习的方向:

  • 深入 pytest 高级用法(fixture、plugin 开发)
  • 学习 Appium 进行移动端自动化
  • 探索 Playwright / Cypress 等新一代自动化工具
  • 结合 AI 辅助生成和维护测试用例
posted @ 2026-04-07 16:04  小小阿狸。  阅读(0)  评论(0)    收藏  举报