feat(base_page): 新增BasePage基础操作
- 优化 is_visible,支持快速状态检查。 - 新增 log_screenshot/log_screenshot_bytes 截图。 - 更新 README.md。 - 其他优化。
This commit is contained in:
15
README.md
15
README.md
@@ -1,6 +1,6 @@
|
|||||||
# AppAutoTest
|
# AppAutoTest
|
||||||
|
|
||||||
设备能力配置 (Capabilities)
|
## 设备能力配置 (Capabilities)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
ANDROID_CAPS = {
|
ANDROID_CAPS = {
|
||||||
@@ -50,3 +50,16 @@ platformVersion:指定设备的Android系统版本(如 "11.0")。虽然不
|
|||||||
unicodeKeyboard 和 resetKeyboard:用于处理中文输入等特殊字符输入。
|
unicodeKeyboard 和 resetKeyboard:用于处理中文输入等特殊字符输入。
|
||||||
|
|
||||||
newCommandTimeout:设置Appium服务器等待客户端发送新命令的超时时间(秒),默认为60秒。在长时间操作中可能需要增加。
|
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 |
|
||||||
@@ -42,8 +42,8 @@ def driver(app_server):
|
|||||||
"platformName": "Android",
|
"platformName": "Android",
|
||||||
"automationName": "uiautomator2",
|
"automationName": "uiautomator2",
|
||||||
"deviceName": "Android",
|
"deviceName": "Android",
|
||||||
"appPackage": "com.bocionline.ibmp",
|
"appPackage": "io.appium.android.apis",
|
||||||
"appActivity": "com.bocionline.ibmp.app.main.launcher.LauncherActivity",
|
"appActivity": "io.appium.android.apis.ApiDemos",
|
||||||
"noReset": False, # 不清除应用数据
|
"noReset": False, # 不清除应用数据
|
||||||
"newCommandTimeout": 60
|
"newCommandTimeout": 60
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,14 @@
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
from typing import Type, TypeVar, List, Tuple
|
from typing import Type, TypeVar, List, Tuple, Optional
|
||||||
|
import allure
|
||||||
|
from pathlib import Path
|
||||||
from appium import webdriver
|
from appium import webdriver
|
||||||
from selenium.common import TimeoutException
|
from selenium.common import TimeoutException
|
||||||
|
|
||||||
from core.driver import CoreDriver
|
from core.driver import CoreDriver
|
||||||
|
from utils.decorators import exception_capture
|
||||||
|
|
||||||
# 定义一个泛型,用于类型推断(IDE 依然会有补全提示)
|
# 定义一个泛型,用于类型推断(IDE 依然会有补全提示)
|
||||||
T = TypeVar('T', bound='BasePage')
|
T = TypeVar('T', bound='BasePage')
|
||||||
@@ -29,7 +31,74 @@ class BasePage(CoreDriver):
|
|||||||
super().__init__(driver)
|
super().__init__(driver)
|
||||||
# 定义常见弹窗的关闭按钮定位
|
# 定义常见弹窗的关闭按钮定位
|
||||||
|
|
||||||
|
def log_screenshot(self, label: str = "步骤截图"):
|
||||||
|
"""
|
||||||
|
业务级截图:执行截图并附加到 Allure 报告。
|
||||||
|
用户可自由手动调用此方法。
|
||||||
|
"""
|
||||||
|
path_str = self.full_screen_screenshot(name=label)
|
||||||
|
|
||||||
|
if path_str:
|
||||||
|
img_path = Path(path_str)
|
||||||
|
if img_path.exists():
|
||||||
|
allure.attach.file(
|
||||||
|
img_path,
|
||||||
|
name=label,
|
||||||
|
attachment_type=allure.attachment_type.PNG
|
||||||
|
)
|
||||||
|
|
||||||
|
def log_screenshot_bytes(self, label: str = "步骤截图"):
|
||||||
|
"""
|
||||||
|
业务级截图:执行截图并附加到 Allure 报告。
|
||||||
|
用户可自由手动调用此方法。
|
||||||
|
"""
|
||||||
|
_img: bytes = self.driver.get_screenshot_as_png()
|
||||||
|
|
||||||
|
allure.attach(
|
||||||
|
_img,
|
||||||
|
name=label,
|
||||||
|
attachment_type=allure.attachment_type.PNG
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- 常用断言逻辑 ---
|
||||||
|
def assert_text(self, by: str, value: str, expected_text: str, timeout: Optional[float] = None) -> 'BasePage':
|
||||||
|
"""
|
||||||
|
断言元素的文本内容是否符合预期。
|
||||||
|
:param by: 定位策略。
|
||||||
|
:param value: 定位值。
|
||||||
|
:param expected_text: 期望的文本。
|
||||||
|
:param timeout: 等待元素可见的超时时间。
|
||||||
|
:return: self,支持链式调用。
|
||||||
|
:raises AssertionError: 如果文本不匹配。
|
||||||
|
"""
|
||||||
|
# 1. 增强报告展示:将断言动作包装为一个清晰的步骤
|
||||||
|
step_name = f"断言校验 | 预期结果: '{expected_text}'"
|
||||||
|
with allure.step(step_name):
|
||||||
|
actual = self.get_text(by, value, timeout)
|
||||||
|
# 2. 动态附件:在报告中直观对比,方便后期排查
|
||||||
|
allure.attach(
|
||||||
|
f"预期值: {expected_text}\n实际值: {actual}",
|
||||||
|
name="文本对比结果",
|
||||||
|
attachment_type=allure.attachment_type.TEXT
|
||||||
|
)
|
||||||
|
# 3. 执行核心断言
|
||||||
|
# 如果断言失败,抛出的 AssertionError 会被 conftest.py 中的 Hook 捕获并截图
|
||||||
|
assert actual == expected_text, f"断言失败: 期望 {expected_text}, 实际 {actual}"
|
||||||
|
logger.info(f"断言通过: 文本匹配 '{actual}'")
|
||||||
|
return self
|
||||||
|
|
||||||
# 这里放全局通用的 Page 属性和逻辑
|
# 这里放全局通用的 Page 属性和逻辑
|
||||||
|
def assert_visible(self, by: str, value: str, msg: str = "元素可见性校验"):
|
||||||
|
"""
|
||||||
|
增强版断言:成功/失败均截图
|
||||||
|
"""
|
||||||
|
with allure.step(f"断言检查: {msg}"):
|
||||||
|
try:
|
||||||
|
element = self.find_element(by, value)
|
||||||
|
assert element.is_displayed()
|
||||||
|
# 成功存证
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
# 封装一些所有页面通用的元动作
|
# 封装一些所有页面通用的元动作
|
||||||
def clear_permission_popups(self):
|
def clear_permission_popups(self):
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ class CoreDriver:
|
|||||||
method = EC.visibility_of_element_located(mark)
|
method = EC.visibility_of_element_located(mark)
|
||||||
|
|
||||||
text = self.explicit_wait(method, timeout).text
|
text = self.explicit_wait(method, timeout).text
|
||||||
logger.info(f"获取到的文本{text}")
|
logger.info(f"获取到的文本: {text}")
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def get_attribute(self, by: str, value: str, name: str, timeout: Optional[float] = None) -> str:
|
def get_attribute(self, by: str, value: str, name: str, timeout: Optional[float] = None) -> str:
|
||||||
@@ -597,22 +597,6 @@ class CoreDriver:
|
|||||||
"""判断当前驱动会话是否仍然存活。"""
|
"""判断当前驱动会话是否仍然存活。"""
|
||||||
return self.driver is not None and self.driver.session_id is not None
|
return self.driver is not None and self.driver.session_id is not None
|
||||||
|
|
||||||
# --- 断言逻辑 ---
|
|
||||||
def assert_text(self, by: str, value: str, expected_text: str, timeout: Optional[float] = None) -> 'CoreDriver':
|
|
||||||
"""
|
|
||||||
断言元素的文本内容是否符合预期。
|
|
||||||
:param by: 定位策略。
|
|
||||||
:param value: 定位值。
|
|
||||||
:param expected_text: 期望的文本。
|
|
||||||
:param timeout: 等待元素可见的超时时间。
|
|
||||||
:return: self,支持链式调用。
|
|
||||||
:raises AssertionError: 如果文本不匹配。
|
|
||||||
"""
|
|
||||||
actual = self.get_text(by, value, timeout)
|
|
||||||
assert actual == expected_text, f"断言失败: 期望 {expected_text}, 实际 {actual}"
|
|
||||||
logger.info(f"断言通过: 文本匹配 '{actual}'")
|
|
||||||
return self
|
|
||||||
|
|
||||||
def quit(self):
|
def quit(self):
|
||||||
"""安全关闭 Appium 驱动并断开连接。"""
|
"""安全关闭 Appium 驱动并断开连接。"""
|
||||||
if self.driver:
|
if self.driver:
|
||||||
|
|||||||
53
page_objects/api_demod_home.py
Normal file
53
page_objects/api_demod_home.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding=utf-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
@author: CNWei,ChenWei
|
||||||
|
@Software: PyCharm
|
||||||
|
@contact: t6g888@163.com
|
||||||
|
@file: test_home
|
||||||
|
@date: 2026/1/30 17:18
|
||||||
|
@desc:
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import allure
|
||||||
|
from appium import webdriver
|
||||||
|
|
||||||
|
from core.base_page import BasePage
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HomePage(BasePage):
|
||||||
|
# 定位参数
|
||||||
|
text = ("accessibility id", "Text")
|
||||||
|
unicode = ("accessibility id", "Unicode")
|
||||||
|
|
||||||
|
def __init__(self, driver: webdriver.Remote):
|
||||||
|
super().__init__(driver)
|
||||||
|
|
||||||
|
@allure.step("点击 “Text ”")
|
||||||
|
def click_text(self):
|
||||||
|
if self.wait_until_visible(*self.text, timeout=1):
|
||||||
|
with allure.step("发现Text,开始执行点击"):
|
||||||
|
# self.log_screenshot_bytes("Text截图").click(*self.text)
|
||||||
|
self.log_screenshot_bytes("Text截图")
|
||||||
|
self.click(*self.text)
|
||||||
|
|
||||||
|
@allure.step("点击 “Unicode ”:{1}")
|
||||||
|
def click_unicode(self, taget):
|
||||||
|
"""执行登录业务逻辑"""
|
||||||
|
# 调用继承自 CoreDriver 的方法(假设你的 CoreDriver 已经被注入或组合)
|
||||||
|
|
||||||
|
if self.wait_until_visible(*self.unicode):
|
||||||
|
self.swipe("left")
|
||||||
|
|
||||||
|
self.click(*self.unicode).log_screenshot()
|
||||||
|
|
||||||
|
@allure.step("获取 “Text ”文本")
|
||||||
|
def get_home_text(self):
|
||||||
|
"""执行登录业务逻辑"""
|
||||||
|
# 调用继承自 CoreDriver 的方法(假设你的 CoreDriver 已经被注入或组合)
|
||||||
|
|
||||||
|
return self.get_text(*self.text)
|
||||||
33
page_objects/api_demos_views.py
Normal file
33
page_objects/api_demos_views.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/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截图")
|
||||||
@@ -9,3 +9,44 @@
|
|||||||
@date: 2026/1/19 14:08
|
@date: 2026/1/19 14:08
|
||||||
@desc:
|
@desc:
|
||||||
"""
|
"""
|
||||||
|
import pytest
|
||||||
|
import allure
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||||
|
def pytest_runtest_makereport(item, call):
|
||||||
|
"""
|
||||||
|
本钩子函数会在每个测试阶段(setup, call, teardown)执行后被调用。
|
||||||
|
item: 测试用例对象
|
||||||
|
call: 测试执行阶段的信息
|
||||||
|
"""
|
||||||
|
# 1. 先执行常规的用例报告生成逻辑
|
||||||
|
outcome = yield
|
||||||
|
report = outcome.get_result()
|
||||||
|
|
||||||
|
# 2. 我们只关注测试执行阶段 ("call")
|
||||||
|
# 如果该阶段失败了(failed),则触发截图
|
||||||
|
if report.when == "call" and report.failed:
|
||||||
|
# 3. 从测试用例中获取 driver 实例
|
||||||
|
# 假设你在 fixture 中注入的参数名为 'driver'
|
||||||
|
driver_instance = item.funcargs.get("driver")
|
||||||
|
|
||||||
|
if driver_instance:
|
||||||
|
try:
|
||||||
|
# 4. 调用你在 CoreDriver 中实现的底层截图方法
|
||||||
|
# 这里的 name 我们可以动态取测试用例的名字
|
||||||
|
case_name = item.name
|
||||||
|
file_path = driver_instance.full_screen_screenshot(name=f"CRASH_{case_name}")
|
||||||
|
|
||||||
|
# 5. 如果路径存在,将其关联到 Allure 报告
|
||||||
|
if file_path:
|
||||||
|
p = Path(file_path)
|
||||||
|
if p.exists():
|
||||||
|
allure.attach.file(
|
||||||
|
source=p,
|
||||||
|
name="【故障现场自动截图】",
|
||||||
|
attachment_type=allure.attachment_type.PNG
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"故障自动截图执行失败: {e}")
|
||||||
53
test_cases/test_api_demos.py
Normal file
53
test_cases/test_api_demos.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding=utf-8
|
||||||
|
|
||||||
|
"""
|
||||||
|
@author: CNWei,ChenWei
|
||||||
|
@Software: PyCharm
|
||||||
|
@contact: t6g888@163.com
|
||||||
|
@file: test_api_demos
|
||||||
|
@date: 2026/1/30 17:42
|
||||||
|
@desc:
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import allure
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from page_objects.api_demod_home import HomePage
|
||||||
|
from page_objects.api_demos_views import ViewsPage
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@allure.epic("ApiDemos")
|
||||||
|
@allure.feature("登录认证模块")
|
||||||
|
class TestApiDemos:
|
||||||
|
# @allure.story("常规登录场景")
|
||||||
|
@allure.title("使用合法账号登录成功")
|
||||||
|
@allure.severity(allure.severity_level.BLOCKER)
|
||||||
|
@allure.description("""
|
||||||
|
验证用户在正常网络环境下:
|
||||||
|
1. 处理初始化弹窗和广告
|
||||||
|
2. 选择证券登录类型
|
||||||
|
3. 输入正确凭证后成功跳转至‘交易’首页
|
||||||
|
""")
|
||||||
|
@allure.link("https://docs.example.com/login_spec", name="登录业务说明文档")
|
||||||
|
@allure.issue("BUG-1001", "已知偶发:部分机型广告Banner无法滑动")
|
||||||
|
def test_api_demos_success(self, driver,user):
|
||||||
|
"""
|
||||||
|
测试场景:使用正确的用户名和密码登录成功
|
||||||
|
"""
|
||||||
|
api_demos = HomePage(driver)
|
||||||
|
|
||||||
|
# 执行业务逻辑
|
||||||
|
api_demos.click_text()
|
||||||
|
api_demos.click_unicode()
|
||||||
|
# 断言部分使用 allure.step 包装,使其在报告中也是一个可读的步骤
|
||||||
|
with allure.step("最终校验:检查是否进入首页并显示‘交易’标题"):
|
||||||
|
actual_text = api_demos.get_home_text()
|
||||||
|
assert actual_text == "Text"
|
||||||
|
|
||||||
|
# 页面跳转
|
||||||
|
api_demos.go_to(ViewsPage).screenshot_views()
|
||||||
|
api_demos.delay(5)
|
||||||
@@ -5,7 +5,6 @@ from typing import Union, Callable
|
|||||||
|
|
||||||
from core.custom_expected_conditions import get_condition
|
from core.custom_expected_conditions import get_condition
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -37,3 +36,19 @@ def resolve_wait_method(func):
|
|||||||
return func(self, method, *args, **kwargs)
|
return func(self, method, *args, **kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def exception_capture(func):
|
||||||
|
"""
|
||||||
|
仅在原子动作失败时,触发 BasePage 层的业务截图逻辑
|
||||||
|
"""
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
# 自动捕获:调用 BasePage 层的 log_screenshot
|
||||||
|
if hasattr(self, "log_screenshot"):
|
||||||
|
self.log_screenshot(f"自动异常捕获{func.__name__}")
|
||||||
|
raise e
|
||||||
|
return wrapper
|
||||||
|
|||||||
Reference in New Issue
Block a user