diff --git a/.gitignore b/.gitignore index 35d7c9b..a2e31d4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,10 +12,9 @@ wheels/ venv/ node_modules/ uv.lock + # --- 屏蔽outputs --- -outputs/logs/*.log -outputs/logs/backups/* -outputs/screenshots/ +outputs/ # --- Allure 报告 --- temp/ diff --git a/README.md b/README.md index e154a6b..9dbdd00 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,176 @@ -# AppAutoTest +# AppAutoTest - 基于 Python 的 App 自动化测试框架 -## 设备能力配置 (Capabilities) +## 1. 简介 -```python -ANDROID_CAPS = { - "platformName": "Android", - "automationName": "uiautomator2", - "deviceName": "Android", - "appPackage": "com.android.settings", - "appActivity": ".Settings", - "noReset": False -} +`AppAutoTest` 是一个基于 Python 构建的健壮且可扩展的移动应用自动化测试框架。它利用 Appium、Pytest 和 Allure 等行业标准工具,为测试 +Android 和 iOS 应用提供了全面的解决方案。该框架采用页面对象模型 (POM) 设计,以提高可维护性和可扩展性,并包含一套实用工具以简化测试开发和执行。 + +## 2. 特性 + +- **跨平台支持**: 原生支持 Android (通过 `UiAutomator2`) 和 iOS (通过 `XCUITest`)。 +- **页面对象模型 (POM)**: 强制分离 UI 元素定位器和测试逻辑,增强代码可读性和可维护性。 +- **丰富的测试报告**: 与 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 # 项目说明。 ``` -| 字段名称 | 字段含义解释 | 示例值说明 | -|----------------|---------------------------------------------|---------------------------------------------------------------------------------------| -| 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 下的一个相对路径。 | +## 4. 前置条件 -获取应用的 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 + cd AppAutoTest + ``` + +2. **安装 Node.js 依赖:** + 此命令将在项目目录中本地安装 Appium 及其相关驱动,如 `package.json` 中所定义。 + ```bash + npm install + ``` + +3. **安装 Python 依赖:** + 使用 `uv` 进行 Python 依赖管理。 + ```bash + # 如果没有安装 uv,请先安装 + # 使用 uv 安装项目依赖 + uv sync + ``` + 不使用 `uv`进行 Python 依赖管理。 + ```bash + #使用 pip 安装项目依赖 + # 以可编辑模式安装(开发时推荐) + pip install -e . + + # 或以普通模式安装 + pip install . + ``` + +## 6. 如何运行测试 + +框架提供了两种主要的测试执行方式。 + +### 方法 1: 使用主入口点 (推荐) + +执行 `main.py` 脚本来运行整个测试套件。此脚本会自动处理所有运行前和运行后的任务。 + +```bash +python main.py ``` -获取应用的 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 +1. 确保所有必要的输出目录已创建。 +2. 归档上次运行的日志文件。 +3. 启动 Appium 服务器 (或连接到现有的)。 +4. 通过 Pytest 运行 `test_cases/` 目录下的所有测试。 +5. 在 `reports/` 目录生成新的 Allure 报告。 -# 2,通过 ADB 命令获取(需应用已安装) -adb shell dumpsys window | findstr mCurrentFocus -# 输出结果:mCurrentFocus=Window{... u0 com.example.myapp/com.example.myapp.MainActivity} -# / 后面的部分 com.example.myapp.MainActivity 就是当前 Activity +### 方法 2: 直接使用 Pytest + +为了更精细的控制,您可以直接从命令行调用 `pytest`。`conftest.py` 中定义的 fixtures 仍将管理 Appium 服务器和驱动会话。 + +```bash +# 运行特定的测试文件并显示详细输出 +pytest -v test_cases/test_wan_android_home.py + +# 运行测试并在第一个失败处停止 +pytest -x + +# 覆盖默认平台并指定设备 UDID +pytest --platform IOS --udid ``` -常用补充字段: -noReset:True/False。是否在会话开始前重置应用状态(例如清除应用数据)。设置为 True 可以避免每次测试都重新登录。 +**可用的自定义命令行参数:** -platformVersion:指定设备的Android系统版本(如 "11.0")。虽然不是必须,但指定后能增强兼容性。 +- `--platform`: 目标平台 (`Android` 或 `IOS`)。默认为 `Android`。 +- `--udid`: 目标设备的唯一设备标识符 (UDID)。 +- `--host`: Appium 服务器的主机地址。默认为 `127.0.0.1`。 +- `--port`: Appium 服务器的端口。默认为 `4723`。 -unicodeKeyboard 和 resetKeyboard:用于处理中文输入等特殊字符输入。 +> 注意:[其他常用参数](./docs/常用参数.md) -newCommandTimeout:设置Appium服务器等待客户端发送新命令的超时时间(秒),默认为60秒。在长时间操作中可能需要增加。 +## 7. 测试报告 -## allure核心属性表 +测试运行后,原始 Allure 结果存储在 `temp/` 目录中。`main.py` 脚本会自动根据这些结果生成 HTML 报告。 -| 属性 | 说明 | 用法示例 | -|---------------------|--------------------------------------|-------------| -| @allure.epic | 顶层分类(如:APP项目名称) | 定义在测试类/项目上 | -| @allure.feature | 功能模块(如:登录模块、交易模块) | 定义在测试类上 | -| @allure.story | 用户场景(如:成功登录、账号锁定) | 定义在测试方法上 | -| @allure.title | 测试用例标题(支持动态显示) | 替换方法名显示在报告中 | -| @allure.severity | "严重程度(BLOCKER, CRITICAL, NORMAL...)" | 用于筛选高优先级用例 | -| @allure.description | 详细描述(支持 Markdown) | 解释测试背景或前提条件 | -| @allure.link | 外部链接(Bug系统、需求文档) | 快速点击跳转 | -| @allure.issue | 缺陷链接(通常会自动带上 ISSUE 前缀) | 追踪已知 Bug | +要查看最新报告,可以使用 Allure 命令行工具启动服务: -Pytest 原生高频参数 和 你的自定义参数。 +```bash +# 此命令将在默认 Web 浏览器中打开报告 +allure serve temp/ +``` -| **分类** | **长参数** | **短参数/别名** | **作用说明** | -|-----------|------------------|------------|------------------------------------| -| **基础运行** | `--verbose` | `-v` | 打印详细运行过程(显示用例名称) | -| | (无) | `-s` | 允许在控制台打印代码里的 `print` 内容 | -| **调试控制** | `--exitfirst` | `-x` | 遇到第一个失败的用例就立即停止测试 | -| | `--maxfail=n` | (无) | 累计失败 `n` 个用例后停止运行 | -| | `--last-failed` | `--lf` | 只运行上次运行失败的用例 | -| **自定义参数** | **`--platform`** | **`-P`** | 指定运行平台 \[Android\/IOS\/别名\]\(可自定义) | -| | **`--udid`** | **`-U`** | 指定手机唯一标识 (可自定义) | -| | **`--host`** | **`-H`** | 指定 Appium 服务器地址 (可自定义) | -| **报告相关** | `--alluredir` | (无) | 指定 Allure 原始数据的存放路径 | +## 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()`: 一个简单但有效的装饰器,在方法成功执行后自动截图,在测试报告中提供可视化的审计跟踪。 \ No newline at end of file diff --git a/conftest.py b/conftest.py index 49677bf..822a55b 100644 --- a/conftest.py +++ b/conftest.py @@ -171,7 +171,7 @@ def pytest_sessionfinish(session: Any, exitstatus: int) -> None: # 收集环境信息 env_info = { "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", "Host": session.config.getoption("--host"), "Python": "3.11+" } diff --git a/core/settings.py b/core/settings.py index 1fe540b..38e730c 100644 --- a/core/settings.py +++ b/core/settings.py @@ -22,7 +22,7 @@ SCREENSHOT_DIR = OUTPUT_DIR / "screenshots" LOG_DIR = OUTPUT_DIR / "logs" LOG_BACKUP_DIR = LOG_DIR / "backups" ALLURE_TEMP = BASE_DIR / "temp" -REPORT_DIR = BASE_DIR / "report" +REPORT_DIR = BASE_DIR / "reports" CONFIG_DIR = BASE_DIR / "config" DATA_DIR = BASE_DIR / "data" diff --git a/docs/AndroidSDK环境配置指南.md b/docs/AndroidSDK环境配置指南.md new file mode 100644 index 0000000..b920afd --- /dev/null +++ b/docs/AndroidSDK环境配置指南.md @@ -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`)。 + ![image-20260110142023709.png](assets/image-20260110142023709.png) +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`)。 + ![image-20260110141432210.png](assets/image-20260110141432210.png) +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 + ``` + ![image-20260110144326458.png](assets/image-20260110144326458.png) + ![image-20260110144556568.png](assets/image-20260110144556568.png) + +> **提示**: 关于环境变量的更多详细设置,请参阅 +> 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`,相关警告应会消失。 \ No newline at end of file diff --git a/docs/assets/image-20260110141432210.png b/docs/assets/image-20260110141432210.png new file mode 100644 index 0000000..3db9c41 Binary files /dev/null and b/docs/assets/image-20260110141432210.png differ diff --git a/docs/assets/image-20260110142023709.png b/docs/assets/image-20260110142023709.png new file mode 100644 index 0000000..2d5826b Binary files /dev/null and b/docs/assets/image-20260110142023709.png differ diff --git a/docs/assets/image-20260110144326458.png b/docs/assets/image-20260110144326458.png new file mode 100644 index 0000000..18c46a2 Binary files /dev/null and b/docs/assets/image-20260110144326458.png differ diff --git a/docs/assets/image-20260110144556568.png b/docs/assets/image-20260110144556568.png new file mode 100644 index 0000000..f21ac7f Binary files /dev/null and b/docs/assets/image-20260110144556568.png differ diff --git a/docs/常用参数.md b/docs/常用参数.md new file mode 100644 index 0000000..80c8d48 --- /dev/null +++ b/docs/常用参数.md @@ -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 原始数据的存放路径 | diff --git a/page_objects/home_page.py b/page_objects/home_page.py deleted file mode 100644 index 7de80f7..0000000 --- a/page_objects/home_page.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/page_objects/login_page.py b/page_objects/login_page.py deleted file mode 100644 index e03a93c..0000000 --- a/page_objects/login_page.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/page_objects/wan_android_home.py b/page_objects/wan_android_home.py index 6f45ecf..5c98506 100644 --- a/page_objects/wan_android_home.py +++ b/page_objects/wan_android_home.py @@ -10,7 +10,6 @@ @desc: """ import logging -import os import allure from appium import webdriver @@ -32,7 +31,6 @@ class HomePage(BasePage): account=("-android uiautomator",'new UiSelector().text("账号")') pass_word=("-android uiautomator",'new UiSelector().text("密码")') - login_button = ("accessibility id", '登录') def __init__(self, driver: webdriver.Remote): @@ -49,21 +47,19 @@ class HomePage(BasePage): @allure.step("登录账号:{1}") def login(self, username, password): """执行登录业务逻辑""" - account_input = {"elementId": '00000000-0000-0ecb-0000-0017000002d8', "text": username} - pass_word_input = {"elementId": '00000000-0000-0ecb-0000-0018000002d8', "text": password} + account_element_id =self.find_element(*self.account).id + 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): 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.login_button).full_screen_screenshot() + self.click(*self.login_button) + + if self.wait_until_visible(*self.tv_name): + self.full_screen_screenshot("登陆成功") + self.long_press(x=636,y=117,duration=300) - # @allure.step("获取 “Text ”文本") - # def get_home_text(self): - # """执行登录业务逻辑"""os.getenv("USER_NAME") os.getenv("PASS_WORD") - # # 调用继承自 CoreDriver 的方法(假设你的 CoreDriver 已经被注入或组合) - # """ - # { "elementId": "00000000-0000-0ecb-0000-0017000002d8", "text": "admintest123456" } - # { "elementId": "00000000-0000-0ecb-0000-0018000002d8", "text": "admin123456" } - # """ - # - # return self.get_text(*self.text) diff --git a/page_objects/wan_android_project.py b/page_objects/wan_android_project.py new file mode 100644 index 0000000..9fe853b --- /dev/null +++ b/page_objects/wan_android_project.py @@ -0,0 +1,39 @@ +#!/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 +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") diff --git a/page_objects/wan_android_sidebar.py b/page_objects/wan_android_sidebar.py deleted file mode 100644 index 847f950..0000000 --- a/page_objects/wan_android_sidebar.py +++ /dev/null @@ -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截图") diff --git a/test_cases/test_settings.py b/test_cases/test_settings.py deleted file mode 100644 index 80c5e17..0000000 --- a/test_cases/test_settings.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/test_cases/test_wan_android_home.py b/test_cases/test_wan_android_home.py index afa50e9..696df49 100644 --- a/test_cases/test_wan_android_home.py +++ b/test_cases/test_wan_android_home.py @@ -5,7 +5,7 @@ @author: CNWei,ChenWei @Software: PyCharm @contact: t6g888@163.com -@file: test_api_demos +@file: test_wan_android_home @date: 2026/1/30 17:42 @desc: """ @@ -13,10 +13,10 @@ import logging import os 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 +from page_objects.wan_android_project import ProjectPage load_dotenv() @@ -24,34 +24,39 @@ load_dotenv() logger = logging.getLogger(__name__) -@allure.epic("ApiDemos") -@allure.feature("登录认证模块") -class TestApiDemos: +@allure.epic("测试用例示例") +@allure.feature("登录模块") +class TestWanAndroidHome: @allure.story("常规登录场景") @allure.title("使用合法账号登录成功") @allure.severity(allure.severity_level.BLOCKER) @allure.description(""" 验证用户在正常网络环境下: 1. 处理初始化弹窗和广告 - 2. 选择证券登录类型 - 3. 输入正确凭证后成功跳转至‘交易’首页 + 2. 输入正确账号密码后成功登录 """) @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): """ 测试场景:使用正确的用户名和密码登录成功 """ - wan = HomePage(driver) + home = HomePage(driver) # 执行业务逻辑 - wan.click_open() - wan.login(os.getenv("USER_NAME"),os.getenv("PASS_WORD")) + home.click_open() + + home.login(os.getenv("USER_NAME"), os.getenv("PASS_WORD")) + # 断言部分使用 allure.step 包装,使其在报告中也是一个可读的步骤 - with allure.step("最终校验:检查是否进入首页并显示‘交易’标题"): - # actual_text = api_demos.get_home_text() - # assert actual_text == "Text" - print("开发中。。。") + with allure.step("断言"): + assert os.getenv("USER_NAME") == 'admintest123456' + + # 页面跳转 - # wan.go_to(ViewsPage).screenshot_views() - wan.delay(5) + with allure.step("验证页面跳转"): + project = home.go_to(ProjectPage) + project.switch_to_project() + project.assert_text(*project.pro_table_title,expected_text='完整项目') + + project.delay(5) diff --git a/test_cases/test_wan_android_project.py b/test_cases/test_wan_android_project.py new file mode 100644 index 0000000..48c054a --- /dev/null +++ b/test_cases/test_wan_android_project.py @@ -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) diff --git a/utils/decorators.py b/utils/decorators.py index 634b88a..b3e590f 100644 --- a/utils/decorators.py +++ b/utils/decorators.py @@ -29,8 +29,23 @@ indent_var = ContextVar("indent_level", default=0) class StepTracer(ContextDecorator): """ - 既是装饰器也是上下文管理器 - 职责:负责计时、日志格式化和异常记录 + 一个结合了上下文管理器和装饰器功能的追踪器。 + + 主要职责是记录一个代码块或函数的执行情况,包括: + - 开始和结束的日志,并根据上下文进行缩进,形成层级结构。 + - 计算并记录执行耗时。 + - 捕获并记录执行期间发生的异常。 + + 可作为上下文管理器使用: + with StepTracer("处理数据"): + ... + + 也可作为装饰器的一部分(通过 step_trace 工厂函数)。 + + Attributes: + step_desc (str): 对当前步骤或操作的描述。 + source (str): 日志记录器的名称。 + func_info (str, optional): 关联的函数信息,用于日志输出。 """ def __init__(self, step_desc, source='wrapper', func_info=None): @@ -40,6 +55,9 @@ class StepTracer(ContextDecorator): self.start_t = None def __enter__(self): + """ + 进入上下文,记录步骤开始,并增加日志缩进层级。 + """ # 1. 获取当前层级并计算前缀 level = indent_var.get() # 使用 " " (空格) 或 "│ " 作为缩进符号 @@ -55,6 +73,10 @@ class StepTracer(ContextDecorator): return self def __exit__(self, exc_type, exc_val, exc_tb): + """ + 退出上下文,记录步骤结束(成功或失败)、耗时,并恢复日志缩进层级。 + 如果发生异常,会记录异常信息但不会抑制它,异常会继续向上传播。 + """ # 3. 恢复层级,层级 -1 level = indent_var.get() - 1 indent_var.set(level) @@ -83,7 +105,23 @@ class StepTracer(ContextDecorator): 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) @@ -113,10 +151,22 @@ def resolve_wait_method(func): def action_screenshot(func): """ - 显式截图装饰器:在方法执行前(或后)立即触发 BasePage 的截图逻辑。 - 用于记录关键操作后的页面状态。 - """ + 装饰器:在被装饰方法成功执行后,自动触发截图。 + 主要用于UI自动化测试中,记录关键业务操作执行后的页面状态。 + 它会调用被装饰对象(通常是 Page Object)的 `attach_screenshot_bytes` 方法。 + + 注意: + - 截图操作在原方法成功返回后执行。 + - 被装饰的实例 (`self`) 必须拥有 `attach_screenshot_bytes` 方法。 + - 如果截图失败,会记录一个警告日志,但不会影响主流程。 + + Args: + func (Callable): 被装饰的函数。 + + Returns: + Callable: 包装后的函数。 + """ @wraps(func) def wrapper(self, *args, **kwargs): # 1. 正常执行原方法 @@ -139,7 +189,21 @@ def action_screenshot(func): def _format_params(func, *args, **kwargs): - """辅助函数:专门处理参数过滤和格式化""" + """ + 辅助函数:格式化函数调用的参数,以便清晰地记录日志。 + + 它会检查函数的签名,并执行以下操作: + 1. 过滤掉实例方法或类方法中的 `self` 或 `cls` 参数。 + 2. 将位置参数和关键字参数格式化为一个可读的字符串。 + + Args: + func (Callable): 目标函数。 + *args: 传递给函数的位置参数。 + **kwargs: 传递给函数的关键字参数。 + + Returns: + str: 格式化后的参数字符串,例如 "arg1, kwarg='value'"。 + """ sig = inspect.signature(func) params = list(sig.parameters.values()) display_args = args[1:] if params and params[0].name in ('self', 'cls') else args @@ -153,12 +217,30 @@ def _format_params(func, *args, **kwargs): def step_trace(step_desc="", source='wrapper'): """ - 通用执行追踪装饰器: - 1. 智能识别并过滤 self/cls 参数 - 2. 记录入参、出参、耗时 - 3. 异常自动捕获并记录 - """ + 装饰器工厂:创建一个通用的执行追踪装饰器。 + 此装饰器利用 `StepTracer` 上下文管理器来提供结构化的日志,记录 + 函数的调用、参数、执行耗时和任何发生的异常。 + + 功能包括: + - 自动格式化并记录函数的输入参数(会智能过滤 `self` 和 `cls`)。 + - 使用 `StepTracer` 生成带缩进的层级式日志,清晰展示调用栈。 + - 记录每个被追踪步骤的开始、成功/失败状态以及执行耗时。 + - 捕获并记录异常,但不会抑制异常,保证上层逻辑可以处理。 + + 用法: + @step_trace("执行用户登录操作") + def login(username, password): + ... + + Args: + step_desc (str, optional): 对被装饰函数所执行操作的描述。 + 如果为空,日志中将只显示函数信息。 + source (str, optional): 指定日志记录器的名称。默认为 'wrapper'。 + + Returns: + Callable: 一个可以装饰函数的装饰器。 + """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs):