feat(base_page): 新增BasePage基础操作
- 优化 is_visible,支持快速状态检查。 - 新增 log_screenshot/log_screenshot_bytes 截图。 - 更新 README.md。 - 其他优化。
This commit is contained in:
17
README.md
17
README.md
@@ -1,6 +1,6 @@
|
||||
# AppAutoTest
|
||||
|
||||
设备能力配置 (Capabilities)
|
||||
## 设备能力配置 (Capabilities)
|
||||
|
||||
```python
|
||||
ANDROID_CAPS = {
|
||||
@@ -49,4 +49,17 @@ platformVersion:指定设备的Android系统版本(如 "11.0")。虽然不
|
||||
|
||||
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",
|
||||
"automationName": "uiautomator2",
|
||||
"deviceName": "Android",
|
||||
"appPackage": "com.bocionline.ibmp",
|
||||
"appActivity": "com.bocionline.ibmp.app.main.launcher.LauncherActivity",
|
||||
"appPackage": "io.appium.android.apis",
|
||||
"appActivity": "io.appium.android.apis.ApiDemos",
|
||||
"noReset": False, # 不清除应用数据
|
||||
"newCommandTimeout": 60
|
||||
}
|
||||
|
||||
@@ -11,12 +11,14 @@
|
||||
"""
|
||||
import logging
|
||||
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 selenium.common import TimeoutException
|
||||
|
||||
from core.driver import CoreDriver
|
||||
from utils.decorators import exception_capture
|
||||
|
||||
# 定义一个泛型,用于类型推断(IDE 依然会有补全提示)
|
||||
T = TypeVar('T', bound='BasePage')
|
||||
@@ -29,7 +31,74 @@ class BasePage(CoreDriver):
|
||||
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 属性和逻辑
|
||||
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):
|
||||
|
||||
@@ -58,7 +58,7 @@ class CoreDriver:
|
||||
"""
|
||||
return f"http://{self._host}:{self._port}"
|
||||
|
||||
def server_config(self, host: str = APPIUM_HOST, port: int = APPIUM_PORT)-> 'CoreDriver':
|
||||
def server_config(self, host: str = APPIUM_HOST, port: int = APPIUM_PORT) -> 'CoreDriver':
|
||||
"""
|
||||
配置服务器信息。支持链式调用。
|
||||
:param host: ip
|
||||
@@ -358,7 +358,7 @@ class CoreDriver:
|
||||
method = EC.visibility_of_element_located(mark)
|
||||
|
||||
text = self.explicit_wait(method, timeout).text
|
||||
logger.info(f"获取到的文本{text}")
|
||||
logger.info(f"获取到的文本: {text}")
|
||||
return text
|
||||
|
||||
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
|
||||
|
||||
# --- 断言逻辑 ---
|
||||
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):
|
||||
"""安全关闭 Appium 驱动并断开连接。"""
|
||||
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截图")
|
||||
@@ -8,4 +8,45 @@
|
||||
@file: conftest
|
||||
@date: 2026/1/19 14:08
|
||||
@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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -36,4 +35,20 @@ def resolve_wait_method(func):
|
||||
|
||||
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