Compare commits
2 Commits
52758940ed
...
6ad6b7ff84
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ad6b7ff84 | |||
| 332deb3666 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -12,10 +12,9 @@ wheels/
|
|||||||
venv/
|
venv/
|
||||||
node_modules/
|
node_modules/
|
||||||
uv.lock
|
uv.lock
|
||||||
|
|
||||||
# --- 屏蔽outputs ---
|
# --- 屏蔽outputs ---
|
||||||
outputs/logs/*.log
|
outputs/
|
||||||
outputs/logs/backups/*
|
|
||||||
outputs/screenshots/
|
|
||||||
|
|
||||||
# --- Allure 报告 ---
|
# --- Allure 报告 ---
|
||||||
temp/
|
temp/
|
||||||
|
|||||||
222
README.md
222
README.md
@@ -1,79 +1,177 @@
|
|||||||
# AppAutoTest
|
# AppAutoTest - 基于 Python 的 App 自动化测试框架
|
||||||
|
|
||||||
## 设备能力配置 (Capabilities)
|
## 1. 简介
|
||||||
|
|
||||||
```python
|
`AppAutoTest` 是一个基于 Python 构建的健壮且可扩展的移动应用自动化测试框架。它利用 Appium、Pytest 和 Allure 等行业标准工具,为测试
|
||||||
ANDROID_CAPS = {
|
Android 和 iOS 应用提供了全面的解决方案。该框架采用页面对象模型 (POM) 设计,以提高可维护性和可扩展性,并包含一套实用工具以简化测试开发和执行。
|
||||||
"platformName": "Android",
|
|
||||||
"automationName": "uiautomator2",
|
## 2. 特性
|
||||||
"deviceName": "Android",
|
|
||||||
"appPackage": "com.android.settings",
|
- **跨平台支持**: 原生支持 Android (通过 `UiAutomator2`) 和 iOS (通过 `XCUITest`)。
|
||||||
"appActivity": ".Settings",
|
- **页面对象模型 (POM)**: 强制分离 UI 元素定位器和测试逻辑,增强代码可读性和可维护性。
|
||||||
"noReset": False
|
- **丰富的测试报告**: 与 Allure 框架无缝集成,生成包含测试步骤、截图、日志和环境信息的详细交互式报告。
|
||||||
}
|
- **自动服务管理**: 智能管理 Appium 服务器生命周期。如果本地服务器未运行,它可以自动启动,或者连接到现有的外部服务器。
|
||||||
|
- **强大的驱动引擎**: 包含核心驱动封装 (`CoreDriver`),通过内置显式等待、流式 API、高级日志记录和健壮的错误处理简化 Appium
|
||||||
|
操作。
|
||||||
|
- **灵活配置**: 轻松管理设备能力 (`config/caps.yaml`)、特定环境参数和敏感数据 (`.env`)。
|
||||||
|
- **可自定义等待条件**: 扩展 Selenium 的标准预期条件,提供用于复杂场景(如等待 Toast 消息或特定元素计数)的自定义可重用等待。
|
||||||
|
- **高级日志与追踪**: 利用自定义装饰器 (`@step_trace`) 自动生成测试步骤的分层日志,包括执行时间、输入参数和状态。
|
||||||
|
- **自动截图**: 在测试失败时自动捕获截图,并允许在任何步骤轻松将截图附加到 Allure 报告中以便快速调试。
|
||||||
|
- **现代依赖管理**: 使用 `uv` (通过 `pyproject.toml`) 进行快速 Python 依赖解析,使用 `npm` (通过 `package.json`) 管理
|
||||||
|
Node.js 依赖。
|
||||||
|
|
||||||
|
## 3. 项目结构
|
||||||
|
|
||||||
|
框架遵循逻辑清晰且模块化的结构,以促进可扩展性和易于导航。
|
||||||
|
|
||||||
|
```text
|
||||||
|
AppAutoTest/
|
||||||
|
├── config/
|
||||||
|
│ └── caps.yaml # 不同平台的 Appium capabilities 配置。
|
||||||
|
├── core/
|
||||||
|
│ ├── base_page.py # 所有页面对象的抽象基类。
|
||||||
|
│ ├── config_loader.py # 加载配置文件 (caps, 环境设置)。
|
||||||
|
│ ├── custom_expected_conditions.py # 定义复杂 UI 状态的自定义等待条件。
|
||||||
|
│ ├── driver.py # 核心 Appium 驱动封装,增强了操作和等待。
|
||||||
|
│ ├── modules.py # 通用枚举定义 (如 AppPlatform, Locator)。
|
||||||
|
│ ├── run_appium.py # 处理 Appium 服务器的生命周期。
|
||||||
|
│ └── settings.py # 全局框架设置 (路径, 超时等)。
|
||||||
|
├── data/ # 数据驱动测试的示例测试数据。
|
||||||
|
├── docs/ # 文档和说明。
|
||||||
|
├── outputs/
|
||||||
|
│ ├── logs/ # 存储测试运行的日志文件。
|
||||||
|
│ └── screenshots/ # 存储测试期间捕获的截图。
|
||||||
|
├── page_objects/ # 对象类。
|
||||||
|
├── reports/ # 存储生成的 Allure HTML 报告。
|
||||||
|
├── temp/ # 原始 Allure 结果的临时目录。
|
||||||
|
├── test_cases/ # 测试脚本。
|
||||||
|
├── utils/
|
||||||
|
│ ├── decorators.py # 用于日志、截图等的自定义装饰器。
|
||||||
|
│ ├── dirs_manager.py # 确保所需目录存在的工具。
|
||||||
|
│ ├── finder.py # 定位策略转换工具。
|
||||||
|
│ └── report_handler.py # Allure 报告生成工具。
|
||||||
|
├── .env # 存储环境变量 (如凭据)。Git 忽略此文件。
|
||||||
|
├── conftest.py # Pytest 的核心配置文件,用于 fixtures 和 hooks。
|
||||||
|
├── main.py # 执行测试套件的主入口点。
|
||||||
|
├── package.json # 定义 Node.js 项目元数据和依赖 (Appium)。
|
||||||
|
├── pytest.ini # Pytest 配置文件 (日志, 标记, 路径)。
|
||||||
|
├── pyproject.toml # Python 项目元数据和依赖定义 (PEP 621)。
|
||||||
|
└── README.md # 项目说明。
|
||||||
```
|
```
|
||||||
|
|
||||||
| 字段名称 | 字段含义解释 | 示例值说明 |
|
## 4. 前置条件
|
||||||
|----------------|---------------------------------------------|---------------------------------------------------------------------------------------|
|
|
||||||
| platformName | 测试的平台/操作系统。这是必填项,告诉自动化框架(如Appium)目标是什么系统。 | "Android" 表示这是一个Android设备。如果是iOS设备,则为 "iOS"。 |
|
|
||||||
| automationName | 使用的自动化驱动引擎。指定底层用哪个工具来驱动设备进行UI交互。 | "uiautomator2" 是当前主流的Android驱动框架,比旧的 "UiAutomator" 更稳定和高效。 |
|
|
||||||
| deviceName | 设备标识/名称。用于在同时连接多台设备时指定目标。 | "Android" 是一个通用标识。在实际测试中,通常用 adb devices 获取的真实设备序列号(如 emulator-5554)来替换它,以确保连接到正确的设备。 |
|
|
||||||
| appPackage | 要测试的应用程序包名。这是应用的唯一标识,就像它在Android系统中的“身份证号”。 | "com.android.settings" 是Android系统“设置”应用的包名。测试你自己的应用时,需替换为你应用的包名。 |
|
|
||||||
| appActivity | 要启动的应用内具体页面。它指定了应用启动后打开的第一个界面(Activity)。 | ".Settings" 是“设置”应用的主界面。前面的点. 表示它是 appPackage 下的一个相对路径。 |
|
|
||||||
|
|
||||||
获取应用的 appPackage 有几种常用方法
|
- **Node.js**: 版本 20+ (如 `package.json` 中指定)。
|
||||||
|
- **Python**: 版本 3.11+ (如 `pyproject.toml` 中指定)。
|
||||||
|
- **Java JDK**: Appium 和 Android SDK 需要。
|
||||||
|
- **Android SDK**: 构建和与 Android 应用交互需要。
|
||||||
|
- **Allure Commandline**: 生成和查看 HTML 报告需要。
|
||||||
|
> 注意:[Android SDK 环境配置指南](docs/AndroidSDK环境配置指南.md)
|
||||||
|
## 5. 安装与运行指南
|
||||||
|
|
||||||
```shell
|
按照以下步骤搭建测试环境。
|
||||||
adb shell pm list packages | findstr your_package_name
|
|
||||||
# adb shell pm list packages -3 仅列出用户安装的第三方应用的包名
|
1. **克隆仓库:**
|
||||||
|
```bash
|
||||||
|
git clone <your-repository-url>
|
||||||
|
cd AppAutoTest
|
||||||
```
|
```
|
||||||
|
|
||||||
获取应用的 appActivity 有几种常用方法
|
2. **安装 Node.js 依赖:**
|
||||||
|
此命令将在项目目录中本地安装 Appium 及其相关驱动,如 `package.json` 中所定义。
|
||||||
```shell
|
```bash
|
||||||
# 1,使用 aapt 工具分析 APK 文件(需有安装包)/build-tools/{version}/aapt
|
npm install
|
||||||
aapt dump badging your_app.apk | findstr launchable-activity
|
|
||||||
# 输出结果:launchable-activity: name='com.example.myapp.MainActivity'
|
|
||||||
# name= 后面的值 'com.example.myapp.MainActivity' 就是你需要的主 Activity
|
|
||||||
|
|
||||||
# 2,通过 ADB 命令获取(需应用已安装)
|
|
||||||
adb shell dumpsys window | findstr mCurrentFocus
|
|
||||||
# 输出结果:mCurrentFocus=Window{... u0 com.example.myapp/com.example.myapp.MainActivity}
|
|
||||||
# / 后面的部分 com.example.myapp.MainActivity 就是当前 Activity
|
|
||||||
```
|
```
|
||||||
|
|
||||||
常用补充字段:
|
3. **安装 Python 依赖:**
|
||||||
noReset:True/False。是否在会话开始前重置应用状态(例如清除应用数据)。设置为 True 可以避免每次测试都重新登录。
|
使用 `uv` 进行 Python 依赖管理。
|
||||||
|
```bash
|
||||||
|
# 如果没有安装 uv,请先安装
|
||||||
|
# 使用 uv 安装项目依赖
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
不使用 `uv`进行 Python 依赖管理。
|
||||||
|
```bash
|
||||||
|
#使用 pip 安装项目依赖
|
||||||
|
# 以可编辑模式安装(开发时推荐)
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
platformVersion:指定设备的Android系统版本(如 "11.0")。虽然不是必须,但指定后能增强兼容性。
|
# 或以普通模式安装
|
||||||
|
pip install .
|
||||||
|
```
|
||||||
|
|
||||||
unicodeKeyboard 和 resetKeyboard:用于处理中文输入等特殊字符输入。
|
## 6. 如何运行测试
|
||||||
|
|
||||||
newCommandTimeout:设置Appium服务器等待客户端发送新命令的超时时间(秒),默认为60秒。在长时间操作中可能需要增加。
|
框架提供了两种主要的测试执行方式。
|
||||||
|
|
||||||
## allure核心属性表
|
### 方法 1: 使用主入口点 (推荐)
|
||||||
|
|
||||||
| 属性 | 说明 | 用法示例 |
|
执行 `main.py` 脚本来运行整个测试套件。此脚本会自动处理所有运行前和运行后的任务。
|
||||||
|---------------------|--------------------------------------|-------------|
|
|
||||||
| @allure.epic | 顶层分类(如:APP项目名称) | 定义在测试类/项目上 |
|
|
||||||
| @allure.feature | 功能模块(如:登录模块、交易模块) | 定义在测试类上 |
|
|
||||||
| @allure.story | 用户场景(如:成功登录、账号锁定) | 定义在测试方法上 |
|
|
||||||
| @allure.title | 测试用例标题(支持动态显示) | 替换方法名显示在报告中 |
|
|
||||||
| @allure.severity | "严重程度(BLOCKER, CRITICAL, NORMAL...)" | 用于筛选高优先级用例 |
|
|
||||||
| @allure.description | 详细描述(支持 Markdown) | 解释测试背景或前提条件 |
|
|
||||||
| @allure.link | 外部链接(Bug系统、需求文档) | 快速点击跳转 |
|
|
||||||
| @allure.issue | 缺陷链接(通常会自动带上 ISSUE 前缀) | 追踪已知 Bug |
|
|
||||||
|
|
||||||
Pytest 原生高频参数 和 你的自定义参数。
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
| **分类** | **长参数** | **短参数/别名** | **作用说明** |
|
此命令将:
|
||||||
|-----------|------------------|------------|------------------------------------|
|
|
||||||
| **基础运行** | `--verbose` | `-v` | 打印详细运行过程(显示用例名称) |
|
1. 确保所有必要的输出目录已创建。
|
||||||
| | (无) | `-s` | 允许在控制台打印代码里的 `print` 内容 |
|
2. 归档上次运行的日志文件。
|
||||||
| **调试控制** | `--exitfirst` | `-x` | 遇到第一个失败的用例就立即停止测试 |
|
3. 启动 Appium 服务器 (或连接到现有的)。
|
||||||
| | `--maxfail=n` | (无) | 累计失败 `n` 个用例后停止运行 |
|
4. 通过 Pytest 运行 `test_cases/` 目录下的所有测试。
|
||||||
| | `--last-failed` | `--lf` | 只运行上次运行失败的用例 |
|
5. 在 `reports/` 目录生成新的 Allure 报告。
|
||||||
| **自定义参数** | **`--platform`** | **`-P`** | 指定运行平台 \[Android\/IOS\/别名\]\(可自定义) |
|
|
||||||
| | **`--udid`** | **`-U`** | 指定手机唯一标识 (可自定义) |
|
### 方法 2: 直接使用 Pytest
|
||||||
| | **`--host`** | **`-H`** | 指定 Appium 服务器地址 (可自定义) |
|
|
||||||
| **报告相关** | `--alluredir` | (无) | 指定 Allure 原始数据的存放路径 |
|
为了更精细的控制,您可以直接从命令行调用 `pytest`。`conftest.py` 中定义的 fixtures 仍将管理 Appium 服务器和驱动会话。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行特定的测试文件并显示详细输出
|
||||||
|
pytest -v test_cases/test_wan_android_home.py
|
||||||
|
|
||||||
|
# 运行测试并在第一个失败处停止
|
||||||
|
pytest -x
|
||||||
|
|
||||||
|
# 覆盖默认平台并指定设备 UDID
|
||||||
|
pytest --platform IOS --udid <your_iphone_udid>
|
||||||
|
```
|
||||||
|
|
||||||
|
**可用的自定义命令行参数:**
|
||||||
|
|
||||||
|
- `--platform`: 目标平台 (`Android` 或 `IOS`)。默认为 `Android`。
|
||||||
|
- `--caps_name`: 设备/平台名称。
|
||||||
|
- `--udid`: 目标设备的唯一设备标识符 (UDID)。
|
||||||
|
- `--host`: Appium 服务器的主机地址。默认为 `127.0.0.1`。
|
||||||
|
- `--port`: Appium 服务器的端口。默认为 `4723`。
|
||||||
|
|
||||||
|
> 注意:[其他常用参数](./docs/常用参数.md)
|
||||||
|
|
||||||
|
## 7. 测试报告
|
||||||
|
|
||||||
|
测试运行后,原始 Allure 结果存储在 `temp/` 目录中。`main.py` 脚本会自动根据这些结果生成 HTML 报告。
|
||||||
|
|
||||||
|
要查看最新报告,可以使用 Allure 命令行工具启动服务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 此命令将在默认 Web 浏览器中打开报告
|
||||||
|
allure serve temp/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 核心概念解释
|
||||||
|
|
||||||
|
#### 页面对象模型 (POM)
|
||||||
|
|
||||||
|
- **`page_objects/`**: 此目录包含 POM 设计的核心。每个类 (例如 `HomePage`)
|
||||||
|
代表应用程序的一个屏幕或重要组件。它负责封装元素定位器和对这些元素执行操作的方法 (例如 `login(user, pwd)`)。
|
||||||
|
- **`core/base_page.py`**: 这是所有页面对象的父类。它提供共享功能,如页面间导航 (`go_to`)、将截图附加到报告 (
|
||||||
|
`attach_screenshot_bytes`) 以及实现通用断言 (`assert_text`)。
|
||||||
|
|
||||||
|
#### 驱动引擎
|
||||||
|
|
||||||
|
- **`core/driver.py`**: 此类是标准 Appium `webdriver.Remote` 的强大封装。它通过集成内置的显式等待增强了基本操作,使测试更稳定,更不容易出现竞争条件。它还为高级移动手势(如
|
||||||
|
`swipe()`、`long_press()` 和 `smart_scroll()`)提供了流畅的接口。
|
||||||
|
|
||||||
|
#### 自定义装饰器
|
||||||
|
|
||||||
|
- **`utils/decorators.py`**: 此模块包含装饰器,可在不干扰逻辑的情况下向测试和页面方法添加强大的横切关注点。
|
||||||
|
- `@step_trace()`: 自动记录任何被装饰函数的进入、退出、参数和执行时间,创建一个清晰的分层日志,这对调试非常有价值。
|
||||||
|
- `@action_screenshot()`: 一个简单但有效的装饰器,在方法成功执行后自动截图,在测试报告中提供可视化的审计跟踪。
|
||||||
@@ -17,3 +17,13 @@ ios:
|
|||||||
autoAcceptAlerts: true
|
autoAcceptAlerts: true
|
||||||
waitForQuiescence: false
|
waitForQuiescence: false
|
||||||
newCommandTimeout: 60
|
newCommandTimeout: 60
|
||||||
|
|
||||||
|
wan_android:
|
||||||
|
platformName: "Android"
|
||||||
|
automationName: "uiautomator2"
|
||||||
|
deviceName: "Android"
|
||||||
|
appPackage: "com.manu.wanandroid"
|
||||||
|
appActivity: "com.manu.wanandroid.ui.main.activity.MainActivity"
|
||||||
|
noReset: false
|
||||||
|
newCommandTimeout: 60
|
||||||
|
# udid: "emulator-5554" # Can be injected via CLI
|
||||||
35
conftest.py
35
conftest.py
@@ -16,12 +16,16 @@ from typing import Generator, Any
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import allure
|
import allure
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from core.run_appium import start_appium_service, stop_appium_service
|
from core.run_appium import start_appium_service, stop_appium_service
|
||||||
from core.driver import CoreDriver
|
from core.driver import CoreDriver
|
||||||
from core.settings import APPIUM_HOST, APPIUM_PORT
|
from core.settings import APPIUM_HOST, APPIUM_PORT
|
||||||
|
from core.enums import AppPlatform
|
||||||
from core.config_loader import get_caps
|
from core.config_loader import get_caps
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
# 注册命令行参数
|
# 注册命令行参数
|
||||||
def pytest_addoption(parser: Any) -> None:
|
def pytest_addoption(parser: Any) -> None:
|
||||||
@@ -29,14 +33,15 @@ def pytest_addoption(parser: Any) -> None:
|
|||||||
注册自定义命令行参数
|
注册自定义命令行参数
|
||||||
:param parser: Pytest 参数解析器对象
|
:param parser: Pytest 参数解析器对象
|
||||||
"""
|
"""
|
||||||
parser.addoption("--platform", action="store", default="Android", help="目标平台: Android or IOS")
|
parser.addoption("--platform", action="store", default="android1", help="目标平台: Android or IOS")
|
||||||
|
parser.addoption("--caps_name", action="store", default=None, help="配置文件中的设备/平台名称")
|
||||||
parser.addoption("--udid", action="store", default=None, help="设备唯一标识")
|
parser.addoption("--udid", action="store", default=None, help="设备唯一标识")
|
||||||
parser.addoption("--host", action="store", default=APPIUM_HOST, help="Appium Server Host")
|
parser.addoption("--host", action="store", default=APPIUM_HOST, help="Appium Server Host")
|
||||||
parser.addoption("--port", action="store", default=str(APPIUM_PORT), help="Appium Server Port")
|
parser.addoption("--port", action="store", default=str(APPIUM_PORT), help="Appium Server Port")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def app_server(request: pytest.FixtureRequest) -> Generator[Any, None, None]:
|
def appium_server(request: pytest.FixtureRequest) -> Generator[Any, None, None]:
|
||||||
"""
|
"""
|
||||||
第一层:管理 Appium Server 进程。
|
第一层:管理 Appium Server 进程。
|
||||||
:param request: Pytest 请求对象
|
:param request: Pytest 请求对象
|
||||||
@@ -52,41 +57,44 @@ def app_server(request: pytest.FixtureRequest) -> Generator[Any, None, None]:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def driver_session(request: pytest.FixtureRequest, app_server: Any) -> Generator[CoreDriver, None, None]:
|
def driver_session(request: pytest.FixtureRequest, appium_server: Any) -> Generator[CoreDriver, None, None]:
|
||||||
"""
|
"""
|
||||||
第二层:全局单例 Driver 管理 (Session Scope)。
|
第二层:全局单例 Driver 管理 (Session Scope)。
|
||||||
负责创建和销毁 Driver,整个测试过程只启动一次 App。
|
负责创建和销毁 Driver,整个测试过程只启动一次 App。
|
||||||
:param request: Pytest 请求对象
|
:param request: Pytest 请求对象
|
||||||
:param app_server: Appium 服务 fixture 依赖
|
:param appium_server: Appium 服务 fixture 依赖
|
||||||
:return: CoreDriver 实例
|
:return: CoreDriver 实例
|
||||||
"""
|
"""
|
||||||
platform = request.config.getoption("--platform")
|
platform: AppPlatform = request.config.getoption("--platform")
|
||||||
|
# 配置名称(caps_name)(决定去 YAML 哪个节点拿数据,默认等于 platform)
|
||||||
|
caps_name = request.config.getoption("--caps_name") or platform
|
||||||
ud_id = request.config.getoption("--udid")
|
ud_id = request.config.getoption("--udid")
|
||||||
host = request.config.getoption("--host")
|
host = request.config.getoption("--host")
|
||||||
port = int(request.config.getoption("--port"))
|
port = int(request.config.getoption("--port"))
|
||||||
|
|
||||||
# 1. 获取基础 Caps
|
# 1. 获取基础 Caps
|
||||||
caps = get_caps(platform)
|
caps = get_caps(caps_name)
|
||||||
|
|
||||||
# 2. 动态注入参数
|
# 2. 动态注入参数
|
||||||
if ud_id: caps["udid"] = ud_id
|
if ud_id: caps["udid"] = ud_id
|
||||||
|
|
||||||
# 将最终生效的 caps 存入 pytest 配置,方便报告读取
|
# 将最终生效的 caps 存入 pytest 配置,方便报告读取
|
||||||
request.config._final_caps = caps
|
request.config._final_caps = caps
|
||||||
|
request.config._caps_name = caps_name
|
||||||
|
|
||||||
# 3. 初始化 Driver
|
# 3. 初始化 Driver
|
||||||
app_helper = CoreDriver()
|
driver_helper = CoreDriver()
|
||||||
app_helper.server_config(host=host, port=port)
|
driver_helper.server_config(host=host, port=port)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app_helper.connect(platform=platform, caps=caps)
|
driver_helper.connect(platform=platform, caps=caps)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pytest.exit(f"无法初始化 Driver: {e}")
|
pytest.exit(f"无法初始化 Driver: {e}")
|
||||||
|
|
||||||
yield app_helper
|
yield driver_helper
|
||||||
|
|
||||||
# 4. 清理
|
# 4. 清理
|
||||||
app_helper.quit()
|
driver_helper.quit()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
@@ -165,13 +173,16 @@ def pytest_sessionfinish(session: Any, exitstatus: int) -> None:
|
|||||||
|
|
||||||
report_dir = session.config.getoption("--alluredir")
|
report_dir = session.config.getoption("--alluredir")
|
||||||
final_caps = getattr(session.config, "_final_caps", {})
|
final_caps = getattr(session.config, "_final_caps", {})
|
||||||
|
caps_name = getattr(session.config, "_caps_name", '')
|
||||||
|
|
||||||
if not report_dir:
|
if not report_dir:
|
||||||
return
|
return
|
||||||
report_path = Path(report_dir)
|
report_path = Path(report_dir)
|
||||||
# 收集环境信息
|
# 收集环境信息
|
||||||
env_info = {
|
env_info = {
|
||||||
"Platform": session.config.getoption("--platform"),
|
"Platform": session.config.getoption("--platform"),
|
||||||
"UDID": final_caps.get("udid") or session.config.getoption("--udid") or "未指定",
|
"UDID": final_caps.get("udid") or session.config.getoption("--udid") or "Not Specified",
|
||||||
|
"CapsName": caps_name,
|
||||||
"Host": session.config.getoption("--host"),
|
"Host": session.config.getoption("--host"),
|
||||||
"Python": "3.11+"
|
"Python": "3.11+"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,59 +9,54 @@
|
|||||||
@date: 2026/1/16 10:52
|
@date: 2026/1/16 10:52
|
||||||
@desc: Pytest 核心配置与 Fixture 管理
|
@desc: Pytest 核心配置与 Fixture 管理
|
||||||
"""
|
"""
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
from utils.data_loader import load_yaml
|
from utils.data_loader import load_yaml
|
||||||
from core.settings import CAPS_CONFIG_PATH, ENV_CONFIG, CURRENT_ENV
|
from core.settings import CAPS_CONFIG_PATH, ENV_CONFIG, CURRENT_ENV
|
||||||
from core.modules import AppPlatform
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_env_config() -> dict[str, str]:
|
def get_env_config(env_name: Optional[str] = None) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
根据当前环境 (CURRENT_ENV) 获取对应的业务配置
|
获取当前运行环境的业务配置信息。
|
||||||
|
|
||||||
|
逻辑:
|
||||||
|
1. 优先使用传入的 `env_name`。
|
||||||
|
2. 若未传入,使用全局设置 `CURRENT_ENV`。
|
||||||
|
3. 若都为空,默认为 "test"。
|
||||||
|
4. 如果目标环境在配置中不存在,强制回退到 "test" 环境并记录警告。
|
||||||
|
|
||||||
|
:param env_name: 指定的环境名称 (e.g., "dev", "prod"),可选。
|
||||||
|
:return: 对应环境的配置字典。
|
||||||
"""
|
"""
|
||||||
return ENV_CONFIG.get(CURRENT_ENV, ENV_CONFIG["test"])
|
target_env = env_name or CURRENT_ENV or "test"
|
||||||
|
|
||||||
|
if target_env not in ENV_CONFIG:
|
||||||
|
logger.warning(f"环境 '{target_env}' 未在配置中定义,将回退到 'test' 环境。")
|
||||||
|
return ENV_CONFIG.get("test", {})
|
||||||
|
|
||||||
|
return ENV_CONFIG[target_env]
|
||||||
|
|
||||||
|
|
||||||
def get_caps(platform: str) -> dict[str, Any]:
|
def get_caps(caps_name: str) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
从 YAML 文件加载 Capabilities 配置
|
从 YAML 配置文件加载指定的 Appium Capabilities。
|
||||||
|
|
||||||
|
:param caps_name: 配置文件中的设备/平台名称 (不区分大小写),例如 "android_pixel"。
|
||||||
|
:return: 该设备对应的 Capabilities 字典。
|
||||||
|
:raises ValueError: 当指定的 caps_name 在配置文件中不存在时。
|
||||||
|
:raises RuntimeError: 当配置文件加载失败或格式错误时。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
all_caps = load_yaml(CAPS_CONFIG_PATH)
|
all_caps = load_yaml(CAPS_CONFIG_PATH)
|
||||||
all_caps = {k.lower(): v for k, v in all_caps.items()}
|
all_caps = {k.lower(): v for k, v in all_caps.items()}
|
||||||
platform_key = platform.lower()
|
caps_key = caps_name.lower()
|
||||||
|
|
||||||
if platform_key not in all_caps:
|
if caps_key not in all_caps:
|
||||||
|
raise ValueError(f"在 {CAPS_CONFIG_PATH} 中找不到平台 '{caps_key}' 的配置")
|
||||||
|
|
||||||
base_caps: dict[str, Any] = {
|
return all_caps[caps_key]
|
||||||
"noReset": False,
|
|
||||||
"newCommandTimeout": 60,
|
|
||||||
}
|
|
||||||
|
|
||||||
match platform_key:
|
|
||||||
case AppPlatform.ANDROID:
|
|
||||||
android_caps = {
|
|
||||||
"platformName": "Android",
|
|
||||||
"automationName": "uiautomator2",
|
|
||||||
"deviceName": "Android",
|
|
||||||
"appPackage": "com.manu.wanandroid",
|
|
||||||
"appActivity": "com.manu.wanandroid.ui.main.activity.MainActivity",
|
|
||||||
}
|
|
||||||
return base_caps | android_caps
|
|
||||||
case AppPlatform.IOS:
|
|
||||||
|
|
||||||
ios_caps = {
|
|
||||||
"platformName": "iOS",
|
|
||||||
"automationName": "XCUITest",
|
|
||||||
"deviceName": "iPhone 14",
|
|
||||||
"bundleId": "com.example.app",
|
|
||||||
"autoAcceptAlerts": True,
|
|
||||||
"waitForQuiescence": False,
|
|
||||||
}
|
|
||||||
return base_caps | ios_caps
|
|
||||||
|
|
||||||
return all_caps[platform_key]
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"无法加载“caps”内容 {CAPS_CONFIG_PATH}: {e}")
|
raise RuntimeError(f"加载 Capabilities 失败 ({CAPS_CONFIG_PATH}): {e}")
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ from selenium.webdriver.common.actions import interaction
|
|||||||
from selenium.webdriver.common.actions.action_builder import ActionBuilder
|
from selenium.webdriver.common.actions.action_builder import ActionBuilder
|
||||||
from selenium.webdriver.common.actions.pointer_input import PointerInput
|
from selenium.webdriver.common.actions.pointer_input import PointerInput
|
||||||
|
|
||||||
|
from core.enums import AppPlatform
|
||||||
|
from core.settings import IMPLICIT_WAIT_TIMEOUT, EXPLICIT_WAIT_TIMEOUT, APPIUM_HOST, APPIUM_PORT, SCREENSHOT_DIR
|
||||||
from utils.finder import by_converter
|
from utils.finder import by_converter
|
||||||
from utils.decorators import resolve_wait_method
|
from utils.decorators import resolve_wait_method
|
||||||
from core.modules import AppPlatform
|
|
||||||
from core.settings import IMPLICIT_WAIT_TIMEOUT, EXPLICIT_WAIT_TIMEOUT, APPIUM_HOST, APPIUM_PORT, SCREENSHOT_DIR
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -623,6 +623,7 @@ class CoreDriver:
|
|||||||
:return: self
|
:return: self
|
||||||
"""
|
"""
|
||||||
return self.switch_to_context('NATIVE_APP')
|
return self.switch_to_context('NATIVE_APP')
|
||||||
|
|
||||||
def full_screen_screenshot(self, name: str | None = None) -> str:
|
def full_screen_screenshot(self, name: str | None = None) -> str:
|
||||||
"""
|
"""
|
||||||
截取当前完整屏幕内容 (自愈逻辑、异常报错首选)
|
截取当前完整屏幕内容 (自愈逻辑、异常报错首选)
|
||||||
|
|||||||
58
core/enums.py
Normal file
58
core/enums.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding=utf-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
@author: CNWei,ChenWei
|
||||||
|
@Software: PyCharm
|
||||||
|
@contact: t6g888@163.com
|
||||||
|
@file: enums
|
||||||
|
@date: 2026/2/27 17:05
|
||||||
|
@desc:
|
||||||
|
"""
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class AppiumStatus(Enum):
|
||||||
|
"""Appium 服务状态枚举"""
|
||||||
|
READY = "服务已启动" # 服务和驱动都加载完成 (HTTP 200 + ready: true)
|
||||||
|
INITIALIZING = "驱动正在加载" # 服务已响应但驱动仍在加载 (HTTP 200 + ready: false)
|
||||||
|
CONFLICT = "端口被其他程序占用" # 端口被其他非 Appium 程序占用
|
||||||
|
OFFLINE = "服务未启动" # 服务未启动
|
||||||
|
ERROR = "内部错误"
|
||||||
|
UNKNOWN = "未知状态"
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceRole(Enum):
|
||||||
|
"""服务角色枚举:定义服务的所有权和生命周期"""
|
||||||
|
MANAGED = "托管模式" # 由本脚本启动,负责清理
|
||||||
|
EXTERNAL = "共享模式" # 复用现有服务,不负责清理
|
||||||
|
NULL = "空模式" # 无效或未初始化的服务
|
||||||
|
|
||||||
|
|
||||||
|
class AppPlatform(Enum):
|
||||||
|
"""
|
||||||
|
定义支持的移动应用平台枚举。
|
||||||
|
"""
|
||||||
|
ANDROID = "android"
|
||||||
|
IOS = "ios"
|
||||||
|
|
||||||
|
|
||||||
|
class Locator(str, Enum):
|
||||||
|
"""
|
||||||
|
定义元素定位策略枚举。
|
||||||
|
继承 str 以便直接作为参数传递给 Selenium/Appium 方法。
|
||||||
|
"""
|
||||||
|
# --- 原有 Selenium 支持 ---
|
||||||
|
ID = "id"
|
||||||
|
NAME = "name"
|
||||||
|
CLASS = "class"
|
||||||
|
TAG = "tag"
|
||||||
|
LINK_TEXT = "link_text"
|
||||||
|
PARTIAL_LINK_TEXT = "partial_link_text"
|
||||||
|
CSS = "css"
|
||||||
|
XPATH = "xpath"
|
||||||
|
# --- Appium 特有支持 ---
|
||||||
|
ACCESSIBILITY_ID = "accessibility_id"
|
||||||
|
AID = "aid" # 简写
|
||||||
|
ANDROID_UIAUTOMATOR = "android_uiautomator"
|
||||||
|
IOS_PREDICATE = "ios_predicate"
|
||||||
@@ -9,33 +9,3 @@
|
|||||||
@date: 2026/1/20 11:54
|
@date: 2026/1/20 11:54
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
class AppPlatform(Enum):
|
|
||||||
"""
|
|
||||||
定义支持的移动应用平台枚举。
|
|
||||||
"""
|
|
||||||
ANDROID = "android"
|
|
||||||
IOS = "ios"
|
|
||||||
|
|
||||||
|
|
||||||
class Locator(str, Enum):
|
|
||||||
"""
|
|
||||||
定义元素定位策略枚举。
|
|
||||||
继承 str 以便直接作为参数传递给 Selenium/Appium 方法。
|
|
||||||
"""
|
|
||||||
# --- 原有 Selenium 支持 ---
|
|
||||||
ID = "id"
|
|
||||||
NAME = "name"
|
|
||||||
CLASS = "class"
|
|
||||||
TAG = "tag"
|
|
||||||
LINK_TEXT = "link_text"
|
|
||||||
PARTIAL_LINK_TEXT = "partial_link_text"
|
|
||||||
CSS = "css"
|
|
||||||
XPATH = "xpath"
|
|
||||||
# --- Appium 特有支持 ---
|
|
||||||
ACCESSIBILITY_ID = "accessibility_id"
|
|
||||||
AID = "aid" # 简写
|
|
||||||
ANDROID_UIAUTOMATOR = "android_uiautomator"
|
|
||||||
IOS_PREDICATE = "ios_predicate"
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
@author: CNWei,ChenWei
|
@author: CNWei,ChenWei
|
||||||
@Software: PyCharm
|
@Software: PyCharm
|
||||||
@contact: t6g888@163.com
|
@contact: t6g888@163.com
|
||||||
@file: test
|
@file: run_appium
|
||||||
@date: 2026/1/12 10:21
|
@date: 2026/1/12 10:21
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
@@ -19,10 +19,11 @@ import sys
|
|||||||
import http.client
|
import http.client
|
||||||
import socket
|
import socket
|
||||||
import json
|
import json
|
||||||
from enum import Enum
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from core.settings import BASE_DIR, APPIUM_HOST, APPIUM_PORT, MAX_RETRIES
|
from core.settings import BASE_DIR, APPIUM_HOST, APPIUM_PORT, MAX_RETRIES
|
||||||
|
from core.enums import AppiumStatus, ServiceRole
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -55,23 +56,6 @@ class AppiumInternalError(AppiumStartupError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AppiumStatus(Enum):
|
|
||||||
"""Appium 服务状态枚举"""
|
|
||||||
READY = "服务已启动" # 服务和驱动都加载完成 (HTTP 200 + ready: true)
|
|
||||||
INITIALIZING = "驱动正在加载" # 服务已响应但驱动仍在加载 (HTTP 200 + ready: false)
|
|
||||||
CONFLICT = "端口被其他程序占用" # 端口被其他非 Appium 程序占用
|
|
||||||
OFFLINE = "服务未启动" # 服务未启动
|
|
||||||
ERROR = "内部错误"
|
|
||||||
UNKNOWN = "未知状态"
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceRole(Enum):
|
|
||||||
"""服务角色枚举:定义服务的所有权和生命周期"""
|
|
||||||
MANAGED = "托管模式" # 由本脚本启动,负责清理
|
|
||||||
EXTERNAL = "共享模式" # 复用现有服务,不负责清理
|
|
||||||
NULL = "空模式" # 无效或未初始化的服务
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_appium_command(host: str, port: int | str) -> List[str]:
|
def resolve_appium_command(host: str, port: int | str) -> List[str]:
|
||||||
"""
|
"""
|
||||||
解析 Appium 可执行文件的绝对路径。
|
解析 Appium 可执行文件的绝对路径。
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ SCREENSHOT_DIR = OUTPUT_DIR / "screenshots"
|
|||||||
LOG_DIR = OUTPUT_DIR / "logs"
|
LOG_DIR = OUTPUT_DIR / "logs"
|
||||||
LOG_BACKUP_DIR = LOG_DIR / "backups"
|
LOG_BACKUP_DIR = LOG_DIR / "backups"
|
||||||
ALLURE_TEMP = BASE_DIR / "temp"
|
ALLURE_TEMP = BASE_DIR / "temp"
|
||||||
REPORT_DIR = BASE_DIR / "report"
|
REPORT_DIR = BASE_DIR / "reports"
|
||||||
CONFIG_DIR = BASE_DIR / "config"
|
CONFIG_DIR = BASE_DIR / "config"
|
||||||
DATA_DIR = BASE_DIR / "data"
|
DATA_DIR = BASE_DIR / "data"
|
||||||
|
|
||||||
|
|||||||
106
docs/AndroidSDK环境配置指南.md
Normal file
106
docs/AndroidSDK环境配置指南.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Android SDK 环境配置指南
|
||||||
|
|
||||||
|
本指南将引导您完成 Appium for Android 测试所需的基础环境——Android SDK 的安装与配置。
|
||||||
|
|
||||||
|
## 1. 创建 SDK 目录
|
||||||
|
|
||||||
|
首先,在您的电脑上选择一个合适的位置(例如 `D:\`),创建一个新目录作为 Android SDK 的根目录。
|
||||||
|
|
||||||
|
```
|
||||||
|
D:\android_sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 安装命令行工具 (Command Line Tools)
|
||||||
|
|
||||||
|
1. 访问 Android Studio [官方下载页面](https://developer.android.google.cn/studio?hl=zh-cn#command-line-tools-only)
|
||||||
|
,在页面底部找到 **"仅命令行工具"** 部分,下载适用于您操作系统的软件包(例如 `commandlinetools-win-xxxxxxx_latest.zip`)。
|
||||||
|

|
||||||
|
2. 解压下载的 `zip` 文件。
|
||||||
|
|
||||||
|
3. 将解压后得到的 `cmdline-tools` 目录移动到您在第一步创建的 `android_sdk` 目录中。
|
||||||
|
|
||||||
|
4. 进入 `android_sdk/cmdline-tools` 目录,在内部创建一个名为 `latest` 的新目录。
|
||||||
|
|
||||||
|
5. 将 `cmdline-tools` 目录内的所有内容(包括 `bin` 目录、`lib` 目录、`NOTICE.txt` 和 `source.properties` 文件)移动到刚刚创建的
|
||||||
|
`latest` 目录中。
|
||||||
|
|
||||||
|
完成后的目录结构应如下所示:
|
||||||
|
|
||||||
|
```
|
||||||
|
android_sdk/
|
||||||
|
├── cmdline-tools/
|
||||||
|
│ └── latest/ ← 必须有这个 'latest' 目录
|
||||||
|
│ ├── bin/
|
||||||
|
│ │ └── sdkmanager
|
||||||
|
│ ├── lib/
|
||||||
|
│ └── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 安装平台工具 (Platform-Tools)
|
||||||
|
|
||||||
|
1. 访问 SDK
|
||||||
|
平台工具版本说明页面,下载最新的[平台工具包](https://developer.android.google.cn/tools/releases/platform-tools?hl=zh-cn#downloads.html)
|
||||||
|
(例如 `platform-tools-latest-windows.zip`)。
|
||||||
|

|
||||||
|
2. 解压下载的 `zip` 文件。
|
||||||
|
|
||||||
|
3. 将解压后得到的 `platform-tools` 目录整个移动到 `android_sdk` 根目录下。
|
||||||
|
|
||||||
|
此时,您的 `android_sdk` 目录结构应如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
android_sdk/
|
||||||
|
├── cmdline-tools/
|
||||||
|
├── platform-tools/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 配置环境变量
|
||||||
|
|
||||||
|
为了让系统能够识别 `adb`、`sdkmanager` 等命令,需要配置以下环境变量:
|
||||||
|
|
||||||
|
1. **新建 `ANDROID_HOME`**:
|
||||||
|
* 变量名: `ANDROID_HOME`
|
||||||
|
* 变量值: `D:\android_sdk` (替换为您的实际路径)
|
||||||
|
|
||||||
|
2. **编辑 `Path` 变量**:
|
||||||
|
在系统变量的 `Path` 中,追加以下两项:
|
||||||
|
```
|
||||||
|
%ANDROID_HOME%/tools
|
||||||
|
%ANDROID_HOME%/tools/bin
|
||||||
|
%ANDROID_HOME%/platform-tools
|
||||||
|
%ANDROID_HOME%/cmdline-tools/latest/bin
|
||||||
|
```
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
> **提示**: 关于环境变量的更多详细设置,请参阅
|
||||||
|
> Android [官方文档](https://developer.android.google.cn/tools/variables?hl=zh-cn)。
|
||||||
|
|
||||||
|
## 5. 验证安装
|
||||||
|
|
||||||
|
打开一个新的命令行窗口(CMD 或 PowerShell),输入以下命令:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
adb --version
|
||||||
|
```
|
||||||
|
|
||||||
|
如果成功显示 Android Debug Bridge 的版本信息,则说明 `platform-tools` 已配置成功。
|
||||||
|
|
||||||
|
## 6. (可选) 配置 Bundletool
|
||||||
|
|
||||||
|
`bundletool` 是 Google 用于处理 Android App Bundles (`.aab` 文件) 的工具。如果 `appium-doctor` 提示
|
||||||
|
`bundletool.jar cannot be found` 警告,您可以按以下步骤处理:
|
||||||
|
|
||||||
|
1. **下载**: 从 bundletool GitHub Releases 页面下载最新的 `bundletool-all-x.x.x.jar` 文件。
|
||||||
|
|
||||||
|
2. **创建目录**: 在您的 `ANDROID_HOME` 目录下(例如 `D:\android_sdk`)创建一个名为 `bundle-tool` 的新文件夹。
|
||||||
|
|
||||||
|
3. **重命名并移动**: 将下载的 `.jar` 文件重命名为 `bundletool.jar`,然后将其移动到 `bundle-tool` 文件夹中。
|
||||||
|
|
||||||
|
4. **配置 PATH**: 将 `bundle-tool` 目录也添加到系统的 `Path` 环境变量中:
|
||||||
|
```
|
||||||
|
%ANDROID_HOME%\bundle-tool
|
||||||
|
```
|
||||||
|
|
||||||
|
完成以上步骤后,重新运行 `appium-doctor`,相关警告应会消失。
|
||||||
BIN
docs/assets/image-20260110141432210.png
Normal file
BIN
docs/assets/image-20260110141432210.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
BIN
docs/assets/image-20260110142023709.png
Normal file
BIN
docs/assets/image-20260110142023709.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
BIN
docs/assets/image-20260110144326458.png
Normal file
BIN
docs/assets/image-20260110144326458.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
BIN
docs/assets/image-20260110144556568.png
Normal file
BIN
docs/assets/image-20260110144556568.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
79
docs/常用参数.md
Normal file
79
docs/常用参数.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# 常用参数说明
|
||||||
|
|
||||||
|
## 设备能力配置 (Capabilities)
|
||||||
|
|
||||||
|
```python
|
||||||
|
ANDROID_CAPS = {
|
||||||
|
"platformName": "Android",
|
||||||
|
"automationName": "uiautomator2",
|
||||||
|
"deviceName": "Android",
|
||||||
|
"appPackage": "com.android.settings",
|
||||||
|
"appActivity": ".Settings",
|
||||||
|
"noReset": False
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段名称 | 字段含义解释 | 示例值说明 |
|
||||||
|
|----------------|---------------------------------------------|---------------------------------------------------------------------------------------|
|
||||||
|
| platformName | 测试的平台/操作系统。这是必填项,告诉自动化框架(如Appium)目标是什么系统。 | "Android" 表示这是一个Android设备。如果是iOS设备,则为 "iOS"。 |
|
||||||
|
| automationName | 使用的自动化驱动引擎。指定底层用哪个工具来驱动设备进行UI交互。 | "uiautomator2" 是当前主流的Android驱动框架,比旧的 "UiAutomator" 更稳定和高效。 |
|
||||||
|
| deviceName | 设备标识/名称。用于在同时连接多台设备时指定目标。 | "Android" 是一个通用标识。在实际测试中,通常用 adb devices 获取的真实设备序列号(如 emulator-5554)来替换它,以确保连接到正确的设备。 |
|
||||||
|
| appPackage | 要测试的应用程序包名。这是应用的唯一标识,就像它在Android系统中的“身份证号”。 | "com.android.settings" 是Android系统“设置”应用的包名。测试你自己的应用时,需替换为你应用的包名。 |
|
||||||
|
| appActivity | 要启动的应用内具体页面。它指定了应用启动后打开的第一个界面(Activity)。 | ".Settings" 是“设置”应用的主界面。前面的点. 表示它是 appPackage 下的一个相对路径。 |
|
||||||
|
|
||||||
|
获取应用的 appPackage 有几种常用方法
|
||||||
|
|
||||||
|
```shell
|
||||||
|
adb shell pm list packages | findstr your_package_name
|
||||||
|
# adb shell pm list packages -3 仅列出用户安装的第三方应用的包名
|
||||||
|
```
|
||||||
|
|
||||||
|
获取应用的 appActivity 有几种常用方法
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# 1,使用 aapt 工具分析 APK 文件(需有安装包)/build-tools/{version}/aapt
|
||||||
|
aapt dump badging your_app.apk | findstr launchable-activity
|
||||||
|
# 输出结果:launchable-activity: name='com.example.myapp.MainActivity'
|
||||||
|
# name= 后面的值 'com.example.myapp.MainActivity' 就是你需要的主 Activity
|
||||||
|
|
||||||
|
# 2,通过 ADB 命令获取(需应用已安装)
|
||||||
|
adb shell dumpsys window | findstr mCurrentFocus
|
||||||
|
# 输出结果:mCurrentFocus=Window{... u0 com.example.myapp/com.example.myapp.MainActivity}
|
||||||
|
# / 后面的部分 com.example.myapp.MainActivity 就是当前 Activity
|
||||||
|
```
|
||||||
|
|
||||||
|
常用补充字段:
|
||||||
|
noReset:True/False。是否在会话开始前重置应用状态(例如清除应用数据)。设置为 True 可以避免每次测试都重新登录。
|
||||||
|
|
||||||
|
platformVersion:指定设备的Android系统版本(如 "11.0")。虽然不是必须,但指定后能增强兼容性。
|
||||||
|
|
||||||
|
unicodeKeyboard 和 resetKeyboard:用于处理中文输入等特殊字符输入。
|
||||||
|
|
||||||
|
newCommandTimeout:设置Appium服务器等待客户端发送新命令的超时时间(秒),默认为60秒。在长时间操作中可能需要增加。
|
||||||
|
|
||||||
|
## allure核心属性表
|
||||||
|
|
||||||
|
| 属性 | 说明 | 用法示例 |
|
||||||
|
|---------------------|--------------------------------------|-------------|
|
||||||
|
| @allure.epic | 顶层分类(如:APP项目名称) | 定义在测试类/项目上 |
|
||||||
|
| @allure.feature | 功能模块(如:登录模块、交易模块) | 定义在测试类上 |
|
||||||
|
| @allure.story | 用户场景(如:成功登录、账号锁定) | 定义在测试方法上 |
|
||||||
|
| @allure.title | 测试用例标题(支持动态显示) | 替换方法名显示在报告中 |
|
||||||
|
| @allure.severity | "严重程度(BLOCKER, CRITICAL, NORMAL...)" | 用于筛选高优先级用例 |
|
||||||
|
| @allure.description | 详细描述(支持 Markdown) | 解释测试背景或前提条件 |
|
||||||
|
| @allure.link | 外部链接(Bug系统、需求文档) | 快速点击跳转 |
|
||||||
|
| @allure.issue | 缺陷链接(通常会自动带上 ISSUE 前缀) | 追踪已知 Bug |
|
||||||
|
|
||||||
|
Pytest 原生高频参数 和 你的自定义参数。
|
||||||
|
|
||||||
|
| **分类** | **长参数** | **短参数/别名** | **作用说明** |
|
||||||
|
|-----------|------------------|------------|------------------------------------|
|
||||||
|
| **基础运行** | `--verbose` | `-v` | 打印详细运行过程(显示用例名称) |
|
||||||
|
| | (无) | `-s` | 允许在控制台打印代码里的 `print` 内容 |
|
||||||
|
| **调试控制** | `--exitfirst` | `-x` | 遇到第一个失败的用例就立即停止测试 |
|
||||||
|
| | `--maxfail=n` | (无) | 累计失败 `n` 个用例后停止运行 |
|
||||||
|
| | `--last-failed` | `--lf` | 只运行上次运行失败的用例 |
|
||||||
|
| **自定义参数** | **`--platform`** | **`-P`** | 指定运行平台 \[Android\/IOS\/别名\]\(可自定义) |
|
||||||
|
| | **`--udid`** | **`-U`** | 指定手机唯一标识 (可自定义) |
|
||||||
|
| | **`--host`** | **`-H`** | 指定 Appium 服务器地址 (可自定义) |
|
||||||
|
| **报告相关** | `--alluredir` | (无) | 指定 Allure 原始数据的存放路径 |
|
||||||
16
main.py
16
main.py
@@ -16,6 +16,7 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from core.settings import LOG_SOURCE, LOG_BACKUP_DIR, ALLURE_TEMP
|
from core.settings import LOG_SOURCE, LOG_BACKUP_DIR, ALLURE_TEMP
|
||||||
|
from core.enums import AppPlatform
|
||||||
from utils.dirs_manager import ensure_dirs_ok
|
from utils.dirs_manager import ensure_dirs_ok
|
||||||
from utils.report_handler import generate_allure_report
|
from utils.report_handler import generate_allure_report
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ def _archive_logs():
|
|||||||
else:
|
else:
|
||||||
print("未找到原始日志文件,跳过备份。")
|
print("未找到原始日志文件,跳过备份。")
|
||||||
|
|
||||||
|
|
||||||
# 日志清理
|
# 日志清理
|
||||||
def _clean_old_logs(backup_dir, keep_count=10):
|
def _clean_old_logs(backup_dir, keep_count=10):
|
||||||
files = sorted(Path(backup_dir).glob("pytest_*.log"), key=lambda p: p.stat().st_mtime)
|
files = sorted(Path(backup_dir).glob("pytest_*.log"), key=lambda p: p.stat().st_mtime)
|
||||||
@@ -54,6 +56,7 @@ def _clean_old_logs(backup_dir, keep_count=10):
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
print(f"清理旧日志失败 {file_to_remove}: {e}")
|
print(f"清理旧日志失败 {file_to_remove}: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _clean_temp_dirs():
|
def _clean_temp_dirs():
|
||||||
"""
|
"""
|
||||||
可选:如果你想在测试前清理掉旧的临时文件
|
可选:如果你想在测试前清理掉旧的临时文件
|
||||||
@@ -64,6 +67,7 @@ def _clean_temp_dirs():
|
|||||||
shutil.rmtree(ALLURE_TEMP, ignore_errors=True)
|
shutil.rmtree(ALLURE_TEMP, ignore_errors=True)
|
||||||
ALLURE_TEMP.mkdir(parents=True, exist_ok=True)
|
ALLURE_TEMP.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
# 1. 创建目录
|
# 1. 创建目录
|
||||||
@@ -73,8 +77,16 @@ def main():
|
|||||||
_archive_logs()
|
_archive_logs()
|
||||||
|
|
||||||
# 3. 执行 Pytest
|
# 3. 执行 Pytest
|
||||||
# 注意:-x 表示遇到错误立即停止,如果是全量回归建议去掉 -x
|
|
||||||
pytest.main(["test_cases", "-x", "-v", f"--alluredir={ALLURE_TEMP}"])
|
args = [
|
||||||
|
"test_cases",
|
||||||
|
"-x", # 注意:-x 表示遇到错误立即停止,如果是全量回归建议去掉 -x
|
||||||
|
"-v",
|
||||||
|
f"--alluredir={ALLURE_TEMP}",
|
||||||
|
f"--platform={AppPlatform.ANDROID.value}",
|
||||||
|
"--caps_name=wan_android"
|
||||||
|
]
|
||||||
|
pytest.main(args)
|
||||||
|
|
||||||
# 4. 生成报告
|
# 4. 生成报告
|
||||||
generate_allure_report()
|
generate_allure_report()
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
"""
|
|
||||||
@author: CNWei,ChenWei
|
|
||||||
@Software: PyCharm
|
|
||||||
@contact: t6g888@163.com
|
|
||||||
@file: home_page
|
|
||||||
@date: 2026/1/26 17:37
|
|
||||||
@desc:
|
|
||||||
"""
|
|
||||||
|
|
||||||
from core.base_page import BasePage
|
|
||||||
class HomePage(BasePage):
|
|
||||||
_LOGOUT_BTN = ("text", "退出登录")
|
|
||||||
_NICKNAME = ("id", "user_nickname")
|
|
||||||
|
|
||||||
def get_nickname(self):
|
|
||||||
return self.driver.get_text(*self._NICKNAME)
|
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
self.driver.click(*self._LOGOUT_BTN)
|
|
||||||
# 【核心:链式跳转】
|
|
||||||
# 退出后回到登录页
|
|
||||||
return self.login_page
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
"""
|
|
||||||
@author: CNWei,ChenWei
|
|
||||||
@Software: PyCharm
|
|
||||||
@contact: t6g888@163.com
|
|
||||||
@file: login_page
|
|
||||||
@date: 2026/1/26 17:34
|
|
||||||
@desc:
|
|
||||||
"""
|
|
||||||
|
|
||||||
from core.base_page import BasePage
|
|
||||||
from page_objects.home_page import HomePage
|
|
||||||
|
|
||||||
class LoginPage(BasePage):
|
|
||||||
# 定位参数私有化,不暴露给外面
|
|
||||||
_USER_FIELD = ("id", "com.app:id/username")
|
|
||||||
_PWD_FIELD = ("id", "com.app:id/password")
|
|
||||||
_LOGIN_BTN = ("id", "com.app:id/btn_login")
|
|
||||||
|
|
||||||
def login_as(self, username, password):
|
|
||||||
"""执行登录业务逻辑"""
|
|
||||||
# 调用继承自 CoreDriver 的方法(假设你的 CoreDriver 已经被注入或组合)
|
|
||||||
self.driver.input(*self._USER_FIELD, text=username)
|
|
||||||
self.driver.input(*self._PWD_FIELD, text=password, sensitive=True)
|
|
||||||
self.driver.click(*self._LOGIN_BTN)
|
|
||||||
|
|
||||||
# 【核心:链式跳转】
|
|
||||||
# 登录成功后,逻辑上应该进入首页,所以返回首页实例
|
|
||||||
return self.to_page(HomePage)
|
|
||||||
@@ -5,12 +5,11 @@
|
|||||||
@author: CNWei,ChenWei
|
@author: CNWei,ChenWei
|
||||||
@Software: PyCharm
|
@Software: PyCharm
|
||||||
@contact: t6g888@163.com
|
@contact: t6g888@163.com
|
||||||
@file: test_home
|
@file: wan_android_home
|
||||||
@date: 2026/1/30 17:18
|
@date: 2026/1/30 17:18
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
import allure
|
import allure
|
||||||
from appium import webdriver
|
from appium import webdriver
|
||||||
@@ -32,7 +31,6 @@ class HomePage(BasePage):
|
|||||||
account = ("-android uiautomator", 'new UiSelector().text("账号")')
|
account = ("-android uiautomator", 'new UiSelector().text("账号")')
|
||||||
pass_word = ("-android uiautomator", 'new UiSelector().text("密码")')
|
pass_word = ("-android uiautomator", 'new UiSelector().text("密码")')
|
||||||
|
|
||||||
|
|
||||||
login_button = ("accessibility id", '登录')
|
login_button = ("accessibility id", '登录')
|
||||||
|
|
||||||
def __init__(self, driver: webdriver.Remote):
|
def __init__(self, driver: webdriver.Remote):
|
||||||
@@ -49,21 +47,18 @@ class HomePage(BasePage):
|
|||||||
@allure.step("登录账号:{1}")
|
@allure.step("登录账号:{1}")
|
||||||
def login(self, username, password):
|
def login(self, username, password):
|
||||||
"""执行登录业务逻辑"""
|
"""执行登录业务逻辑"""
|
||||||
account_input = {"elementId": '00000000-0000-0ecb-0000-0017000002d8', "text": username}
|
account_element_id = self.find_element(*self.account).id
|
||||||
pass_word_input = {"elementId": '00000000-0000-0ecb-0000-0018000002d8', "text": password}
|
account_input = {"elementId": account_element_id, "text": username}
|
||||||
|
|
||||||
|
pwd_element_id = self.find_element(*self.pass_word).id
|
||||||
|
pass_word_input = {"elementId": pwd_element_id, "text": password}
|
||||||
|
|
||||||
if self.wait_until_visible(*self.login_button):
|
if self.wait_until_visible(*self.login_button):
|
||||||
self.click(*self.account).driver.execute_script('mobile: type', account_input)
|
self.click(*self.account).driver.execute_script('mobile: type', account_input)
|
||||||
self.click(*self.pass_word).driver.execute_script('mobile: type', pass_word_input)
|
self.click(*self.pass_word).driver.execute_script('mobile: type', pass_word_input)
|
||||||
|
|
||||||
self.click(*self.login_button).full_screen_screenshot()
|
self.click(*self.login_button)
|
||||||
|
|
||||||
# @allure.step("获取 “Text ”文本")
|
if self.wait_until_visible(*self.tv_name):
|
||||||
# def get_home_text(self):
|
self.full_screen_screenshot("登陆成功")
|
||||||
# """执行登录业务逻辑"""os.getenv("USER_NAME") os.getenv("PASS_WORD")
|
self.long_press(x=636, y=117, duration=300)
|
||||||
# # 调用继承自 CoreDriver 的方法(假设你的 CoreDriver 已经被注入或组合)
|
|
||||||
# """
|
|
||||||
# { "elementId": "00000000-0000-0ecb-0000-0017000002d8", "text": "admintest123456" }
|
|
||||||
# { "elementId": "00000000-0000-0ecb-0000-0018000002d8", "text": "admin123456" }
|
|
||||||
# """
|
|
||||||
#
|
|
||||||
# return self.get_text(*self.text)
|
|
||||||
|
|||||||
41
page_objects/wan_android_project.py
Normal file
41
page_objects/wan_android_project.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding=utf-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
@author: CNWei,ChenWei
|
||||||
|
@Software: PyCharm
|
||||||
|
@contact: t6g888@163.com
|
||||||
|
@file: wan_android_project
|
||||||
|
@date: 2026/1/30 17:37
|
||||||
|
@desc:
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import allure
|
||||||
|
from appium import webdriver
|
||||||
|
|
||||||
|
from core.base_page import BasePage
|
||||||
|
from utils.decorators import StepTracer
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectPage(BasePage):
|
||||||
|
# 定位参数
|
||||||
|
project_title = ("-android uiautomator", 'new UiSelector().text("项目")')
|
||||||
|
pro_table_title = ("-android uiautomator", 'new UiSelector().text("完整项目")')
|
||||||
|
|
||||||
|
def __init__(self, driver: webdriver.Remote):
|
||||||
|
super().__init__(driver)
|
||||||
|
|
||||||
|
@allure.step("切换到“项目”页面")
|
||||||
|
def switch_to_project(self):
|
||||||
|
self.click(*self.project_title).attach_screenshot_bytes()
|
||||||
|
|
||||||
|
@allure.step("滑动切换“项目”内容")
|
||||||
|
@StepTracer("页面滑动")
|
||||||
|
def slide_views(self):
|
||||||
|
with allure.step("向左滑动3次"):
|
||||||
|
with StepTracer("开始划了"):
|
||||||
|
for _ in range(3):
|
||||||
|
self.swipe("left")
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
"""
|
|
||||||
@author: CNWei,ChenWei
|
|
||||||
@Software: PyCharm
|
|
||||||
@contact: t6g888@163.com
|
|
||||||
@file: test_views
|
|
||||||
@date: 2026/1/30 17:37
|
|
||||||
@desc:
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import allure
|
|
||||||
from appium import webdriver
|
|
||||||
|
|
||||||
from core.base_page import BasePage
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ViewsPage(BasePage):
|
|
||||||
# 定位参数
|
|
||||||
views = ("accessibility id","Views")
|
|
||||||
|
|
||||||
def __init__(self, driver: webdriver.Remote):
|
|
||||||
super().__init__(driver)
|
|
||||||
|
|
||||||
@allure.step("截图 “Views ”")
|
|
||||||
def screenshot_views(self):
|
|
||||||
if self.wait_until_visible(*self.views):
|
|
||||||
with allure.step("发现Views,开始执行点击"):
|
|
||||||
self.log_screenshot_bytes("Text截图")
|
|
||||||
@@ -7,7 +7,6 @@ requires-python = ">=3.11"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"allure-pytest==2.13.5",
|
"allure-pytest==2.13.5",
|
||||||
"appium-python-client>=5.2.4",
|
"appium-python-client>=5.2.4",
|
||||||
"loguru>=0.7.3",
|
|
||||||
"pytest>=8.3.5",
|
"pytest>=8.3.5",
|
||||||
"PyYAML>=6.0.1",
|
"PyYAML>=6.0.1",
|
||||||
"openpyxl>=3.1.2",
|
"openpyxl>=3.1.2",
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
"""
|
|
||||||
@author: CNWei,ChenWei
|
|
||||||
@Software: PyCharm
|
|
||||||
@contact: t6g888@163.com
|
|
||||||
@file: test_settings
|
|
||||||
@date: 2026/1/16 15:56
|
|
||||||
@desc:
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import allure
|
|
||||||
|
|
||||||
from utils.decorators import step_trace
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
@allure.epic("中银国际移动端重构项目")
|
|
||||||
@allure.feature("登认证模块")
|
|
||||||
@step_trace("验证失败",)
|
|
||||||
def test_settings_page_display(driver):
|
|
||||||
"""
|
|
||||||
测试设置页面是否成功加载
|
|
||||||
"""
|
|
||||||
# 此时 driver 已经通过 fixture 完成了初始化
|
|
||||||
current_act = driver.current_activity
|
|
||||||
logger.info(f"捕获到当前 Activity: {current_act}")
|
|
||||||
|
|
||||||
assert ".app.main.launcher.LauncherActivity" in current_act
|
|
||||||
|
|
||||||
|
|
||||||
def test_wifi_entry_exists(driver):
|
|
||||||
"""
|
|
||||||
简单的元素查找示例
|
|
||||||
"""
|
|
||||||
# 这里的 driver 就是 appium.webdriver.Remote 实例
|
|
||||||
# 假设我们要查找“网络”相关的 ID
|
|
||||||
# el = driver.find_element(by='id', value='android:id/title')
|
|
||||||
assert driver.session_id is not None
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
@author: CNWei,ChenWei
|
@author: CNWei,ChenWei
|
||||||
@Software: PyCharm
|
@Software: PyCharm
|
||||||
@contact: t6g888@163.com
|
@contact: t6g888@163.com
|
||||||
@file: test_api_demos
|
@file: test_wan_android_home
|
||||||
@date: 2026/1/30 17:42
|
@date: 2026/1/30 17:42
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
@@ -13,45 +13,46 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import allure
|
import allure
|
||||||
import pytest
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from page_objects.wan_android_home import HomePage
|
|
||||||
from page_objects.wan_android_sidebar import ViewsPage
|
|
||||||
|
|
||||||
load_dotenv()
|
from page_objects.wan_android_home import HomePage
|
||||||
|
from page_objects.wan_android_project import ProjectPage
|
||||||
|
|
||||||
# 配置日志
|
# 配置日志
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@allure.epic("ApiDemos")
|
@allure.epic("测试用例示例")
|
||||||
@allure.feature("登录认证模块")
|
@allure.feature("登录模块")
|
||||||
class TestApiDemos:
|
class TestWanAndroidHome:
|
||||||
@allure.story("常规登录场景")
|
@allure.story("常规登录场景")
|
||||||
@allure.title("使用合法账号登录成功")
|
@allure.title("使用合法账号登录成功")
|
||||||
@allure.severity(allure.severity_level.BLOCKER)
|
@allure.severity(allure.severity_level.BLOCKER)
|
||||||
@allure.description("""
|
@allure.description("""
|
||||||
验证用户在正常网络环境下:
|
验证用户在正常网络环境下:
|
||||||
1. 处理初始化弹窗和广告
|
1. 处理初始化弹窗和广告
|
||||||
2. 选择证券登录类型
|
2. 输入正确账号密码后成功登录
|
||||||
3. 输入正确凭证后成功跳转至‘交易’首页
|
|
||||||
""")
|
""")
|
||||||
@allure.link("https://docs.example.com/login_spec", name="登录业务说明文档")
|
@allure.link("https://docs.example.com/login_spec", name="登录业务说明文档")
|
||||||
@allure.issue("BUG-1001", "已知偶发:部分机型广告Banner无法滑动")
|
@allure.issue("BUG-1001", "已知偶发:我是一个bug")
|
||||||
def test_api_demos_success(self, driver):
|
def test_api_demos_success(self, driver):
|
||||||
"""
|
"""
|
||||||
测试场景:使用正确的用户名和密码登录成功
|
测试场景:使用正确的用户名和密码登录成功
|
||||||
"""
|
"""
|
||||||
wan = HomePage(driver)
|
home = HomePage(driver)
|
||||||
|
|
||||||
# 执行业务逻辑
|
# 执行业务逻辑
|
||||||
wan.click_open()
|
home.click_open()
|
||||||
wan.login(os.getenv("USER_NAME"),os.getenv("PASS_WORD"))
|
|
||||||
|
home.login(os.getenv("USER_NAME"), os.getenv("PASS_WORD"))
|
||||||
|
|
||||||
# 断言部分使用 allure.step 包装,使其在报告中也是一个可读的步骤
|
# 断言部分使用 allure.step 包装,使其在报告中也是一个可读的步骤
|
||||||
with allure.step("最终校验:检查是否进入首页并显示‘交易’标题"):
|
with allure.step("断言"):
|
||||||
# actual_text = api_demos.get_home_text()
|
assert os.getenv("USER_NAME") == 'admintest123456'
|
||||||
# assert actual_text == "Text"
|
|
||||||
print("开发中。。。")
|
|
||||||
# 页面跳转
|
# 页面跳转
|
||||||
# wan.go_to(ViewsPage).screenshot_views()
|
with allure.step("验证页面跳转"):
|
||||||
wan.delay(5)
|
project = home.go_to(ProjectPage)
|
||||||
|
project.switch_to_project()
|
||||||
|
project.assert_text(*project.pro_table_title, expected_text='完整项目')
|
||||||
|
|
||||||
|
project.delay(5)
|
||||||
|
|||||||
49
test_cases/test_wan_android_project.py
Normal file
49
test_cases/test_wan_android_project.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding=utf-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
@author: CNWei,ChenWei
|
||||||
|
@Software: PyCharm
|
||||||
|
@contact: t6g888@163.com
|
||||||
|
@file: test_wan_android_project
|
||||||
|
@date: 2026/1/30 17:42
|
||||||
|
@desc:
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import allure
|
||||||
|
|
||||||
|
from page_objects.wan_android_project import ProjectPage
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@allure.epic("测试用例示例")
|
||||||
|
@allure.feature("项目模块")
|
||||||
|
class TestWanAndroidProject:
|
||||||
|
@allure.story("项目切换场景")
|
||||||
|
@allure.title("切换项目页面")
|
||||||
|
@allure.severity(allure.severity_level.NORMAL)
|
||||||
|
@allure.description("""
|
||||||
|
验证滑动切换项目页面展示:
|
||||||
|
1. XXXXX
|
||||||
|
2. XXXXXXXXXXX
|
||||||
|
""")
|
||||||
|
@allure.link("https://docs.example.com/login_spec", name="项目业务说明文档")
|
||||||
|
@allure.issue("BUG-1002", "已知偶发:我是一个bug")
|
||||||
|
def test_project_slide_success(self, driver):
|
||||||
|
"""
|
||||||
|
测试场景:验证页面滑动
|
||||||
|
"""
|
||||||
|
project = ProjectPage(driver)
|
||||||
|
|
||||||
|
# 执行业务逻辑
|
||||||
|
project.switch_to_project()
|
||||||
|
project.slide_views()
|
||||||
|
|
||||||
|
# 断言部分使用 allure.step 包装,使其在报告中也是一个可读的步骤
|
||||||
|
with allure.step("断言"):
|
||||||
|
assert 1 == 1
|
||||||
|
|
||||||
|
project.delay(5)
|
||||||
@@ -14,6 +14,7 @@ import pytest
|
|||||||
from appium.webdriver.common.appiumby import AppiumBy
|
from appium.webdriver.common.appiumby import AppiumBy
|
||||||
from utils.finder import by_converter, register_custom_finder, converter
|
from utils.finder import by_converter, register_custom_finder, converter
|
||||||
|
|
||||||
|
|
||||||
class TestFinderConverter:
|
class TestFinderConverter:
|
||||||
|
|
||||||
def setup_method(self):
|
def setup_method(self):
|
||||||
@@ -83,5 +84,6 @@ class TestFinderConverter:
|
|||||||
with pytest.raises(ValueError, match="Invalid selector type"):
|
with pytest.raises(ValueError, match="Invalid selector type"):
|
||||||
by_converter(None) # type: ignore
|
by_converter(None) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
pytest.main(["-v", __file__])
|
pytest.main(["-v", __file__])
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
"""
|
|
||||||
@author: CNWei,ChenWei
|
|
||||||
@Software: PyCharm
|
|
||||||
@contact: t6g888@163.com
|
|
||||||
@file: test
|
|
||||||
@date: 2026/1/14 10:12
|
|
||||||
@desc:
|
|
||||||
"""
|
|
||||||
|
|
||||||
# import pytest
|
|
||||||
from enum import Enum
|
|
||||||
from typing import TypeVar
|
|
||||||
from utils.logger import logger, trace_step
|
|
||||||
|
|
||||||
|
|
||||||
# --- 模拟业务逻辑 ---
|
|
||||||
|
|
||||||
class ServiceRole(Enum):
|
|
||||||
MANAGED = "受控模式"
|
|
||||||
EXTERNAL = "共享模式"
|
|
||||||
NULL = "无效模式"
|
|
||||||
|
|
||||||
|
|
||||||
class AppiumService:
|
|
||||||
def __init__(self, device_id: str, role: ServiceRole):
|
|
||||||
self.device_id = device_id
|
|
||||||
self.role = role
|
|
||||||
# 使用 bind 为该实例的所有日志绑定特定的设备 ID
|
|
||||||
self._log = logger.bind(source=f"Dev:{device_id}")
|
|
||||||
|
|
||||||
@trace_step(step_desc="停止服务", source="Appium")
|
|
||||||
def stop(self, force=False):
|
|
||||||
"""演示类方法追踪及逻辑分支"""
|
|
||||||
self._log.info(f"正在尝试停止服务,强制模式={force}")
|
|
||||||
|
|
||||||
if self.role == ServiceRole.EXTERNAL:
|
|
||||||
self._log.warning("外部服务,跳过清理进程")
|
|
||||||
return "SKIPPED"
|
|
||||||
|
|
||||||
if self.role == ServiceRole.MANAGED:
|
|
||||||
self._log.success("已发送 SIGTERM 信号清理进程")
|
|
||||||
return "SUCCESS"
|
|
||||||
|
|
||||||
raise RuntimeError("无法停止处于未知状态的服务")
|
|
||||||
|
|
||||||
@trace_step("简单打印")
|
|
||||||
def simple_log(self, msg: str):
|
|
||||||
self._log.info(f"消息回显: {msg}")
|
|
||||||
|
|
||||||
|
|
||||||
# --- 独立函数演示 ---
|
|
||||||
|
|
||||||
@trace_step("执行数据计算", source="Calc")
|
|
||||||
def calculate_data(a: int, b: int):
|
|
||||||
if b == 0:
|
|
||||||
raise ZeroDivisionError("除数不能为 0")
|
|
||||||
return a / b
|
|
||||||
|
|
||||||
|
|
||||||
@trace_step("空值返回测试")
|
|
||||||
def return_none():
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# --- 测试场景覆盖 ---
|
|
||||||
|
|
||||||
def run_scenarios():
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
print("🚀 开始执行全场景日志覆盖测试")
|
|
||||||
print("=" * 50 + "\n")
|
|
||||||
|
|
||||||
# 1. 覆盖:正常类方法调用 (过滤 self)
|
|
||||||
logger.info(">>> 场景 1: 正常类方法 (MANAGED 模式)")
|
|
||||||
svc1 = AppiumService("emulator-5554", ServiceRole.MANAGED)
|
|
||||||
svc1.stop(force=True)
|
|
||||||
|
|
||||||
# 2. 覆盖:类方法不同返回值
|
|
||||||
logger.info(">>> 场景 2: 共享模式跳过清理")
|
|
||||||
svc2 = AppiumService("iPhone_15", ServiceRole.EXTERNAL)
|
|
||||||
svc2.stop()
|
|
||||||
|
|
||||||
# 3. 覆盖:异常捕获 (自动记录错误日志并向上抛出)
|
|
||||||
logger.info(">>> 场景 3: 异常捕获测试")
|
|
||||||
try:
|
|
||||||
calculate_data(10, 0)
|
|
||||||
except ZeroDivisionError:
|
|
||||||
logger.warning("主流程已捕获预期的计算异常")
|
|
||||||
|
|
||||||
# 4. 覆盖:复杂参数与 None 返回
|
|
||||||
logger.info(">>> 场景 4: 复杂参数与 None 返回")
|
|
||||||
return_none()
|
|
||||||
|
|
||||||
# 5. 覆盖:未定义状态导致的崩溃
|
|
||||||
logger.info(">>> 场景 5: 业务逻辑崩溃测试")
|
|
||||||
svc3 = AppiumService("Unknown_Device", ServiceRole.NULL)
|
|
||||||
try:
|
|
||||||
svc3.stop()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"捕获到业务逻辑崩溃:{e}")
|
|
||||||
|
|
||||||
# 6. 覆盖:原生 logger 与装饰器 logger 混合
|
|
||||||
logger.info(">>> 场景 6: 验证自定义 source 标签")
|
|
||||||
# 这里会使用 setup_logger 中定义的默认 'System' 标签
|
|
||||||
logger.debug("这是一条调试级别的原始日志")
|
|
||||||
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
print("✅ 测试场景执行完毕,请检查 logs 文件夹中的 .log 文件")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_scenarios()
|
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
"""
|
"""
|
||||||
import yaml
|
import yaml
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, List
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
def load_yaml(file_path: Path | str) -> dict[str, Any] | List[Any]:
|
def load_yaml(file_path: Path | str) -> dict[str, Any] | list[Any]:
|
||||||
"""
|
"""
|
||||||
加载 YAML 文件
|
加载 YAML 文件
|
||||||
:param file_path: 文件路径
|
:param file_path: 文件路径
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import time
|
|||||||
import inspect
|
import inspect
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Union, Callable
|
from typing import Union, Callable
|
||||||
|
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from contextlib import ContextDecorator
|
from contextlib import ContextDecorator
|
||||||
|
|
||||||
@@ -29,17 +28,36 @@ indent_var = ContextVar("indent_level", default=0)
|
|||||||
|
|
||||||
class StepTracer(ContextDecorator):
|
class StepTracer(ContextDecorator):
|
||||||
"""
|
"""
|
||||||
既是装饰器也是上下文管理器
|
一个结合了上下文管理器和装饰器功能的追踪器。
|
||||||
职责:负责计时、日志格式化和异常记录
|
|
||||||
|
主要职责是记录一个代码块或函数的执行情况,包括:
|
||||||
|
- 开始和结束的日志,并根据上下文进行缩进,形成层级结构。
|
||||||
|
- 计算并记录执行耗时。
|
||||||
|
- 捕获并记录执行期间发生的异常。
|
||||||
|
|
||||||
|
可作为上下文管理器使用:
|
||||||
|
with StepTracer("处理数据"):
|
||||||
|
...
|
||||||
|
|
||||||
|
也可作为装饰器的一部分(通过 step_trace 工厂函数)。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, step_desc, source='wrapper', func_info=None):
|
def __init__(self, step_desc, source='wrapper', func_info=None):
|
||||||
|
"""
|
||||||
|
初始化 StepTracer。
|
||||||
|
:param step_desc: 对当前步骤或操作的描述。
|
||||||
|
:param source: 日志记录器的名称。
|
||||||
|
:param func_info: 关联的函数信息,用于日志输出。
|
||||||
|
"""
|
||||||
self.step_desc = step_desc
|
self.step_desc = step_desc
|
||||||
self.logger = logging.getLogger(source)
|
self.logger = logging.getLogger(source)
|
||||||
self.func_info = func_info
|
self.func_info = func_info
|
||||||
self.start_t = None
|
self.start_t = None
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
"""
|
||||||
|
进入上下文,记录步骤开始,并增加日志缩进层级。
|
||||||
|
"""
|
||||||
# 1. 获取当前层级并计算前缀
|
# 1. 获取当前层级并计算前缀
|
||||||
level = indent_var.get()
|
level = indent_var.get()
|
||||||
# 使用 " " (空格) 或 "│ " 作为缩进符号
|
# 使用 " " (空格) 或 "│ " 作为缩进符号
|
||||||
@@ -55,6 +73,10 @@ class StepTracer(ContextDecorator):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""
|
||||||
|
退出上下文,记录步骤结束(成功或失败)、耗时,并恢复日志缩进层级。
|
||||||
|
如果发生异常,会记录异常信息但不会抑制它,异常会继续向上传播。
|
||||||
|
"""
|
||||||
# 3. 恢复层级,层级 -1
|
# 3. 恢复层级,层级 -1
|
||||||
level = indent_var.get() - 1
|
level = indent_var.get() - 1
|
||||||
indent_var.set(level)
|
indent_var.set(level)
|
||||||
@@ -83,7 +105,23 @@ class StepTracer(ContextDecorator):
|
|||||||
|
|
||||||
def resolve_wait_method(func):
|
def resolve_wait_method(func):
|
||||||
"""
|
"""
|
||||||
装饰器:将字符串形式的等待条件解析为可调用的 EC 对象
|
装饰器:将字符串形式的等待条件解析为可调用的 Expected Condition (EC) 对象。
|
||||||
|
|
||||||
|
这个装饰器用于修饰那些接受一个 `method` 参数的方法(通常是等待方法)。
|
||||||
|
如果 `method` 是一个字符串,它会尝试将其解析为一个预定义的 `expected_conditions`。
|
||||||
|
|
||||||
|
字符串格式支持:
|
||||||
|
- "key": 直接映射到一个无参数的 EC。
|
||||||
|
- "key:arg1,arg2": 映射到一个需要参数的 EC,参数以逗号分隔。
|
||||||
|
|
||||||
|
解析逻辑委托给 `core.custom_expected_conditions.get_condition` 函数。
|
||||||
|
如果解析失败,会记录错误并重新抛出异常。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func (Callable): 被装饰的函数。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Callable: 包装后的函数。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
@@ -113,8 +151,21 @@ def resolve_wait_method(func):
|
|||||||
|
|
||||||
def action_screenshot(func):
|
def action_screenshot(func):
|
||||||
"""
|
"""
|
||||||
显式截图装饰器:在方法执行前(或后)立即触发 BasePage 的截图逻辑。
|
装饰器:在被装饰方法成功执行后,自动触发截图。
|
||||||
用于记录关键操作后的页面状态。
|
|
||||||
|
主要用于UI自动化测试中,记录关键业务操作执行后的页面状态。
|
||||||
|
它会调用被装饰对象(通常是 Page Object)的 `attach_screenshot_bytes` 方法。
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- 截图操作在原方法成功返回后执行。
|
||||||
|
- 被装饰的实例 (`self`) 必须拥有 `attach_screenshot_bytes` 方法。
|
||||||
|
- 如果截图失败,会记录一个警告日志,但不会影响主流程。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func (Callable): 被装饰的函数。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Callable: 包装后的函数。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
@@ -139,7 +190,21 @@ def action_screenshot(func):
|
|||||||
|
|
||||||
|
|
||||||
def _format_params(func, *args, **kwargs):
|
def _format_params(func, *args, **kwargs):
|
||||||
"""辅助函数:专门处理参数过滤和格式化"""
|
"""
|
||||||
|
辅助函数:格式化函数调用的参数,以便清晰地记录日志。
|
||||||
|
|
||||||
|
它会检查函数的签名,并执行以下操作:
|
||||||
|
1. 过滤掉实例方法或类方法中的 `self` 或 `cls` 参数。
|
||||||
|
2. 将位置参数和关键字参数格式化为一个可读的字符串。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func (Callable): 目标函数。
|
||||||
|
*args: 传递给函数的位置参数。
|
||||||
|
**kwargs: 传递给函数的关键字参数。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 格式化后的参数字符串,例如 "arg1, kwarg='value'"。
|
||||||
|
"""
|
||||||
sig = inspect.signature(func)
|
sig = inspect.signature(func)
|
||||||
params = list(sig.parameters.values())
|
params = list(sig.parameters.values())
|
||||||
display_args = args[1:] if params and params[0].name in ('self', 'cls') else args
|
display_args = args[1:] if params and params[0].name in ('self', 'cls') else args
|
||||||
@@ -153,10 +218,29 @@ def _format_params(func, *args, **kwargs):
|
|||||||
|
|
||||||
def step_trace(step_desc="", source='wrapper'):
|
def step_trace(step_desc="", source='wrapper'):
|
||||||
"""
|
"""
|
||||||
通用执行追踪装饰器:
|
装饰器工厂:创建一个通用的执行追踪装饰器。
|
||||||
1. 智能识别并过滤 self/cls 参数
|
|
||||||
2. 记录入参、出参、耗时
|
此装饰器利用 `StepTracer` 上下文管理器来提供结构化的日志,记录
|
||||||
3. 异常自动捕获并记录
|
函数的调用、参数、执行耗时和任何发生的异常。
|
||||||
|
|
||||||
|
功能包括:
|
||||||
|
- 自动格式化并记录函数的输入参数(会智能过滤 `self` 和 `cls`)。
|
||||||
|
- 使用 `StepTracer` 生成带缩进的层级式日志,清晰展示调用栈。
|
||||||
|
- 记录每个被追踪步骤的开始、成功/失败状态以及执行耗时。
|
||||||
|
- 捕获并记录异常,但不会抑制异常,保证上层逻辑可以处理。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
@step_trace("执行用户登录操作")
|
||||||
|
def login(username, password):
|
||||||
|
...
|
||||||
|
|
||||||
|
Args:
|
||||||
|
step_desc (str, optional): 对被装饰函数所执行操作的描述。
|
||||||
|
如果为空,日志中将只显示函数信息。
|
||||||
|
source (str, optional): 指定日志记录器的名称。默认为 'wrapper'。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Callable: 一个可以装饰函数的装饰器。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
@author: CNWei,ChenWei
|
@author: CNWei,ChenWei
|
||||||
@Software: PyCharm
|
@Software: PyCharm
|
||||||
@contact: t6g888@163.com
|
@contact: t6g888@163.com
|
||||||
@file: path_manager
|
@file: dirs_manager
|
||||||
@date: 2026/2/3 10:52
|
@date: 2026/2/3 10:52
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -5,11 +5,12 @@
|
|||||||
@author: CNWei,ChenWei
|
@author: CNWei,ChenWei
|
||||||
@Software: PyCharm
|
@Software: PyCharm
|
||||||
@contact: t6g888@163.com
|
@contact: t6g888@163.com
|
||||||
@file: locator_utils
|
@file: finder
|
||||||
@date: 2026/1/20 15:40
|
@date: 2026/1/20 15:40
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
from typing import Literal, Final
|
from typing import Literal, Final
|
||||||
|
|
||||||
from appium.webdriver.common.appiumby import AppiumBy
|
from appium.webdriver.common.appiumby import AppiumBy
|
||||||
|
|
||||||
ByType = Literal[
|
ByType = Literal[
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
日志与执行追踪模块使用指南
|
|
||||||
本模块基于 Loguru 封装,专为自动化测试项目设计,提供工业级的日志记录、多线程安全保障以及业务步骤自动追踪能力。
|
|
||||||
|
|
||||||
🌟 核心特性
|
|
||||||
全局一致性:统一日志格式,控制台带颜色显示,方便快速定位。
|
|
||||||
|
|
||||||
自动执行追踪:使用 @trace_step 装饰器,自动记录方法入参、出参、耗时及执行状态。
|
|
||||||
|
|
||||||
智能参数解析:装饰器自动识别并隐藏 self 和 cls 参数,保护日志整洁。
|
|
||||||
|
|
||||||
上下文透传:支持通过 .bind() 绑定设备 ID 或模块标识,解决多机并行日志混杂问题。
|
|
||||||
|
|
||||||
分层标识:默认区分系统日志 (System) 与业务任务日志 (task)。
|
|
||||||
|
|
||||||
异步安全:支持多进程/多线程安全写入,不影响 Appium 执行性能。
|
|
||||||
|
|
||||||
🚀 快速上手
|
|
||||||
1. 基础日志记录
|
|
||||||
在任何模块中直接导入 logger 即可使用。
|
|
||||||
```python
|
|
||||||
from utils.logger import logger
|
|
||||||
|
|
||||||
logger.info("这是一条普通信息")
|
|
||||||
logger.debug("调试模式下的详细信息")
|
|
||||||
logger.error("记录一个错误")
|
|
||||||
```
|
|
||||||
2. 使用装饰器追踪业务步骤 (@trace_step)
|
|
||||||
在 PageObject 方法或任何业务函数上添加装饰器,即可获得全链路追踪。
|
|
||||||
```python
|
|
||||||
from utils.logger import trace_step
|
|
||||||
|
|
||||||
@trace_step("用户登录")
|
|
||||||
def login(username, password):
|
|
||||||
# 执行逻辑...
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 日志输出:
|
|
||||||
# 🚀 [START] 用户登录 -> login('admin', '****')
|
|
||||||
# ✅ [SUCCESS] 用户登录 | 耗时: 1.25s | 返回: True
|
|
||||||
```
|
|
||||||
3. 多机并行:上下文透传 (.bind)
|
|
||||||
在 Appium 并行测试中,用于区分不同设备的日志流。
|
|
||||||
```python
|
|
||||||
from utils.logger import logger
|
|
||||||
|
|
||||||
class BasePage:
|
|
||||||
def __init__(self, driver, device_id):
|
|
||||||
self.driver = driver
|
|
||||||
# 绑定设备 ID 到当前实例的 logger
|
|
||||||
self.log = logger.bind(source=device_id)
|
|
||||||
|
|
||||||
def click_element(self, loc):
|
|
||||||
self.log.info(f"点击元素: {loc}")
|
|
||||||
|
|
||||||
# 日志输出:
|
|
||||||
# | INFO | Pixel_6 | base_page:click_element:15 - 点击元素: id=login_btn
|
|
||||||
# | INFO | S22 | base_page:click_element:15 - 点击元素: id=login_btn
|
|
||||||
```
|
|
||||||
🛠️ 进阶配置
|
|
||||||
标识符说明 (source 字段)
|
|
||||||
日志格式中包含一个 source 字段(占位符为 magenta 颜色),用于区分日志来源:
|
|
||||||
|
|
||||||
System: (默认) 框架底层日志、驱动初始化等。
|
|
||||||
|
|
||||||
task: (装饰器默认) 具体的业务测试步骤。
|
|
||||||
|
|
||||||
自定义: 通过 @trace_step(source="SQL") 或 logger.bind(source="API") 自定义。
|
|
||||||
|
|
||||||
日志存储
|
|
||||||
路径: 项目根目录 /logs/。
|
|
||||||
|
|
||||||
滚动: 每天午夜 00:00 自动切割。
|
|
||||||
|
|
||||||
保留: 默认保留最近 30 天 的日志。
|
|
||||||
|
|
||||||
⚠️ 注意事项
|
|
||||||
不要在装饰器内手动接收返回值:@trace_step 已经自动处理了函数的返回值记录。
|
|
||||||
|
|
||||||
优先使用 self.log:在 PageObject 类中,请务必使用 self.log.info() 而非全局 logger.info(),以确保 bind 的上下文信息(如设备 ID)能正确显示。
|
|
||||||
|
|
||||||
希望这套日志系统能显著提升您的调试效率和项目质量!如有任何疑问,请随时查阅 utils/logger.py 源码。
|
|
||||||
Reference in New Issue
Block a user