Appium 移动端自动化测试(十二):日志报告、依赖注入与框架设计
本篇是 Appium 系列的收官之作,讲解日志集成、测试报告生成、依赖注入,以及完整的框架设计。
一、日志集成
1.1 测试日志管理器
import logging
import os
from datetime import datetime
class AppiumTestLogger:
"""Appium 测试日志管理器"""
def __init__(self, logger_name="appium_test", log_dir="logs"):
self.logger = logging.getLogger(logger_name)
self.logger.setLevel(logging.DEBUG)
self.log_dir = log_dir
if self.logger.handlers:
return
os.makedirs(log_dir, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 控制台输出
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S"
))
# 文件输出
file_handler = logging.FileHandler(
f"{log_dir}/test_{timestamp}.log", encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(
"%(asctime)s [%(levelname)s] %(name)s - %(message)s"
))
self.logger.addHandler(console)
self.logger.addHandler(file_handler)
def step(self, name, detail=""):
msg = f"[STEP] {name}"
if detail:
msg += f" | {detail}"
self.logger.info(msg)
def device_info(self, driver):
caps = driver.capabilities
info = (
f"设备: {caps.get('deviceName', 'N/A')} | "
f"平台: {caps.get('platformName', 'N/A')} {caps.get('platformVersion', '')} | "
f"UDID: {caps.get('udid', 'N/A')}"
)
self.logger.info(info)
def assertion(self, expected, actual, passed=True):
status = "PASS" if passed else "FAIL"
self.logger.info(f"[ASSERT] {status} | Expected: {expected} | Actual: {actual}")
def error(self, message):
self.logger.error(f"[ERROR] {message}")
def screenshot(self, filepath):
self.logger.info(f"[SCREENSHOT] {filepath}")
logger = AppiumTestLogger()
1.2 Page 对象日志集成
class LoggedBasePage(BasePage):
"""带日志的 BasePage"""
def click(self, locator):
logger.step("点击", str(locator))
try:
super().click(locator)
except Exception as e:
logger.error(f"点击失败: {locator} | {e}")
self.screenshot("click_error")
raise
def input_text(self, locator, text):
logger.step("输入", f"{locator} = '{text}'")
super().input_text(locator, text)
def swipe_up(self, duration=300):
logger.step("滑动", "向上")
super().swipe_up(duration)
1.3 pytest 日志集成
# conftest.py
import pytest
@pytest.fixture(autouse=True)
def log_test_lifecycle(request, driver):
"""自动记录测试开始和结束"""
test_name = request.node.name
logger.step("TEST START", test_name)
logger.device_info(driver)
yield
logger.step("TEST END", test_name)
driver.save_screenshot(f"screenshots/{test_name}.png")
二、测试报告
2.1 Allure 报告(推荐)
pip install allure-pytest
brew install allure # macOS
import allure
from appium.webdriver.common.appiumby import AppiumBy
@allure.feature("用户模块")
@allure.story("登录功能")
@allure.title("使用正确凭据登录")
@allure.severity(allure.severity_level.BLOCKER)
def test_login_success(driver):
with allure.step("打开登录页面"):
login = LoginPage(driver)
with allure.step("输入用户名"):
login.type_text(login.USERNAME, "admin")
with allure.step("输入密码"):
login.type_text(login.PASSWORD, "123456")
with allure.step("点击登录"):
home = login.login("admin", "123456")
with allure.step("验证登录成功"):
assert home.is_logged_in()
# 附加截图
allure.attach(
driver.get_screenshot_as_png(),
name="登录成功截图",
attachment_type=allure.attachment_type.PNG
)
# 附加设备信息
allure.attach(
str(driver.capabilities),
name="设备信息",
attachment_type=allure.attachment_type.TEXT
)
2.2 自定义报告生成器
import json
from datetime import datetime
from pathlib import Path
class AppiumTestReport:
"""Appium 测试报告生成器"""
def __init__(self, output_dir="reports"):
self.output_dir = Path(output_dir)
self.output_dir.mkdir(exist_ok=True)
self.results = []
self.device_info = {}
def set_device(self, driver):
"""记录设备信息"""
caps = driver.capabilities
self.device_info = {
"device": caps.get("deviceName"),
"platform": caps.get("platformName"),
"version": caps.get("platformVersion"),
}
def add_result(self, name, status, duration, error=None, screenshot=None):
"""添加测试结果"""
self.results.append({
"name": name,
"status": status,
"duration": f"{duration:.2f}s",
"device": self.device_info.get("device", "N/A"),
"error": str(error) if error else None,
"screenshot": screenshot,
"timestamp": datetime.now().strftime("%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")
html = f"""<!DOCTYPE html>
<html><head><meta charset="utf-8">
<title>Appium 测试报告</title>
<style>
body {{ font-family: -apple-system, sans-serif; margin: 40px; background: #f5f5f5; }}
.summary {{ background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }}
.device {{ color: #666; margin-bottom: 10px; }}
.stat {{ display: inline-block; padding: 8px 16px; margin-right: 8px; border-radius: 4px; color: white; }}
.pass {{ background: #22c55e; }} .fail {{ background: #ef4444; }}
table {{ width: 100%; border-collapse: collapse; background: white; border-radius: 8px; }}
th, td {{ padding: 10px; text-align: left; border-bottom: 1px solid #e5e7eb; }}
th {{ background: #f9fafb; }} .error {{ color: #ef4444; font-size: 12px; }}
</style></head><body>
<h1>Appium 移动端测试报告</h1>
<div class="device">
设备: {self.device_info.get("device", "N/A")} |
平台: {self.device_info.get("platform", "N/A")} {self.device_info.get("version", "")}
</div>
<div class="summary">
<span class="stat pass">通过: {passed}</span>
<span class="stat fail">失败: {failed}</span>
<span>总计: {total} | 通过率: {passed/max(total,1)*100:.1f}%</span>
</div>
<table>
<tr><th>用例</th><th>状态</th><th>设备</th><th>耗时</th><th>时间</th><th>错误</th></tr>"""
for r in self.results:
html += f"""
<tr>
<td>{r['name']}</td>
<td class="{r['status']}">{r['status'].upper()}</td>
<td>{r['device']}</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 测试上下文管理
class TestContext:
"""测试上下文:管理所有共享对象"""
def __init__(self, config):
self.config = config
self._driver = None
self._logger = None
self._pages = {}
self._report = None
@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 = AppiumTestLogger()
return self._logger
@property
def report(self):
if self._report is None:
self._report = AppiumTestReport()
return self._report
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):
from appium import webdriver
from appium.options.common.base import AppiumOptions
options = AppiumOptions()
options.load_capabilities(self.config["capabilities"])
if self.config.get("remote_url"):
return webdriver.Remote(self.config["remote_url"], options=options)
else:
return webdriver.Remote(
f"http://127.0.0.1:{self.config.get('port', 4723)}",
options=options
)
def cleanup(self):
if self._driver:
self._driver.quit()
self._driver = None
3.2 pytest Fixture 依赖注入
# conftest.py
import pytest
import yaml
@pytest.fixture(scope="session")
def config():
with open("config/config.yaml", "r", encoding="utf-8") as f:
return yaml.safe_load(f)
@pytest.fixture(scope="session")
def report():
return AppiumTestReport()
@pytest.fixture(scope="function")
def driver(config):
from appium import webdriver
from appium.options.common.base import AppiumOptions
options = AppiumOptions()
options.load_capabilities(config["capabilities"])
options.load_capabilities({
"appium:noReset": True,
"appium:unicodeKeyboard": True,
"appium:resetKeyboard": True,
})
server = config.get("remote_url", f"http://127.0.0.1:{config.get('port', 4723)}")
driver = webdriver.Remote(server, options=options)
driver.implicitly_wait(config.get("implicit_wait", 10))
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, report):
report.set_device(driver)
start = time.time()
try:
login_page.login("admin", "123456")
assert home_page.is_logged_in()
report.add_result("test_login", "pass", time.time() - start)
except Exception as e:
report.add_result("test_login", "fail", time.time() - start, error=e)
raise
3.3 配置文件管理
# config/config.yaml
# 基础配置
port: 4723
implicit_wait: 10
remote_url: null # 本地运行
# Android 配置
android:
capabilities:
platformName: Android
appium:deviceName: Pixel 6
appium:automationName: UiAutomator2
appium:app: /absolute/path/to/app.apk
appium:udid: emulator-5554
appium:noReset: true
appium:unicodeKeyboard: true
appium:resetKeyboard: true
appium:newCommandTimeout: 120
# iOS 配置
ios:
capabilities:
platformName: iOS
appium:deviceName: iPhone 15
appium:automationName: XCUITest
appium:app: /absolute/path/to/app.ipa
appium:noReset: true
appium:newCommandTimeout: 120
# BrowserStack 云测试
browserstack:
remote_url: "https://hub-cloud.browserstack.com/wd/hub"
capabilities:
platformName: Android
appium:deviceName: Samsung Galaxy S24
appium:platformVersion: "14"
appium:automationName: UiAutomator2
appium:app: bs://app-hash
bstack:options:
userName: "${BS_USERNAME}"
accessKey: "${BS_ACCESS_KEY}"
project: My Project
build: CI Build
video: true
networkLogs: true
# config_loader.py
import yaml
import os
class ConfigLoader:
"""配置加载器"""
def __init__(self, config_dir="config"):
self.config_dir = config_dir
def load(self, env="local"):
"""加载配置"""
with open(f"{self.config_dir}/config.yaml", "r", encoding="utf-8") as f:
config = yaml.safe_load(f)
# 环境变量覆盖
if os.getenv("BROWSERSTACK_USERNAME"):
config["remote_url"] = config["browserstack"]["remote_url"]
config["capabilities"] = config["browserstack"]["capabilities"]
config["capabilities"]["bstack:options"]["userName"] = os.getenv("BROWSERSTACK_USERNAME")
config["capabilities"]["bstack:options"]["accessKey"] = os.getenv("BROWSERSTACK_ACCESS_KEY")
if os.getenv("UDID"):
config["capabilities"]["appium:udid"] = os.getenv("UDID")
return config
四、CI/CD 框架集成
4.1 完整 CI 流程
代码提交 → 单元测试 → 编译 APK/IPA → 启动 Appium → 运行测试 → 生成报告 → 通知
4.2 GitHub Actions 完整配置
name: Appium Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
appium-tests:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- device: "Pixel 6"
version: "14"
- device: "Pixel 7"
version: "13"
steps:
- uses: actions/checkout@v4
- name: Setup
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
npm install -g appium
appium driver install uiautomator2
pip install -r requirements.txt
pip install pytest allure-pytest
- name: Start Appium
run: appium -p 4723 &
- name: Run tests
run: |
pytest tests/ \
--alluredir=allure-results \
-v \
--device=${{ matrix.device }} \
--platform-version=${{ matrix.version }}
env:
APPIUM_SERVER: "http://localhost:4723"
APP_PATH: "/path/to/app.apk"
- name: Generate report
if: always()
run: allure generate allure-results -o allure-report --clean
- name: Upload report
uses: actions/upload-artifact@v4
if: always()
with:
name: report-${{ matrix.device }}
path: allure-report/
4.3 测试结果通知
class TestNotifier:
"""测试结果通知"""
@staticmethod
def send_webhook(webhook_url, results, device_info=""):
"""发送到钉钉/飞书 Webhook"""
import requests
total = len(results)
passed = sum(1 for r in results if r["status"] == "pass")
failed = total - passed
color = "green" if failed == 0 else "red"
message = (
f"Appium 测试报告\n"
f"设备: {device_info}\n"
f"结果: {passed}/{total} 通过 ({passed/max(total,1)*100:.0f}%)\n"
)
if failed > 0:
message += "失败用例:\n"
for r in results:
if r["status"] == "fail":
message += f"- {r['name']}: {r['error']}\n"
requests.post(webhook_url, json={
"msgtype": "text",
"text": {"content": message}
})
五、完整框架项目结构
appium-framework/
├── config/
│ ├── config.yaml # 主配置
│ ├── android_caps.yaml # Android 专用配置
│ └── ios_caps.yaml # iOS 专用配置
├── pages/ # Page Object
│ ├── __init__.py
│ ├── base_page.py # BasePage
│ ├── cross_platform_page.py # 跨平台 Page
│ ├── platform_locator.py # 跨平台定位器
│ ├── login_page.py
│ ├── home_page.py
│ ├── search_page.py
│ └── components/
│ ├── bottom_nav.py
│ └── popup_handler.py
├── tests/ # 测试用例
│ ├── conftest.py
│ ├── test_login.py
│ ├── test_search.py
│ └── test_profile.py
├── data/ # 测试数据
│ ├── login_data.csv
│ ├── test_scenarios.yaml
│ └── test_data.json
├── utils/ # 工具类
│ ├── logger.py
│ ├── report.py
│ ├── config_loader.py
│ ├── gesture_helper.py
│ ├── popup_handler.py
│ └── notifier.py
├── reports/ # 测试报告
├── logs/ # 日志
├── screenshots/ # 截图
├── videos/ # 录屏
├── requirements.txt
├── pytest.ini
└── README.md
六、系列总结
本系列 12 篇文章完整覆盖了 Appium 移动端自动化测试的知识体系:
| 篇章 | 主题 |
|---|---|
| 01 | Appium 简介、环境搭建与架构原理 |
| 02 | 第一个测试脚本与元素定位策略 |
| 03 | 常用 Appium API 详解 |
| 04 | 高级元素定位技巧 |
| 05 | 等待机制与脚本优化 |
| 06 | 处理特殊情况(弹窗、Hybrid App、多上下文) |
| 07 | Appium 高级特性(手势、设备操作) |
| 08 | 并行测试与 CI/CD 集成 |
| 09 | 问题排查与性能测试 |
| 10 | Page Object Model 设计模式 |
| 11 | 数据驱动测试 |
| 12 | 日志报告、依赖注入与框架设计 |
掌握这些知识,你已经具备了构建企业级 Appium 移动端自动化测试框架的完整能力。
继续学习的方向:
- 深入学习 Espresso(Android)和 XCUITest(iOS)原生测试框架
- 探索 Appium 2.0 的新特性和插件系统
- 结合 AI 辅助生成测试用例和维护定位器
- 学习 Maestro、Detox 等新一代移动端测试工具

浙公网安备 33010602011771号