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 辅助生成和维护测试用例

浙公网安备 33010602011771号