说到 UI 自动化,有些人肯定会想,我们项目已有在接口自动化,为什么还要做 UI 自动化?
提到 UI 自动化,大家就会提到:
下面我们通过讲如何做 UI 自动化来解决这 2 个问题。
前面提到的 2 个问题相比接口自动化来说确实是存在的,我们需要做的是尽可能提高我们的 ROI 和稳定性。
我们先看下 UI 自动化收益及成本的计算公式:
自动化收益 = 有效迭代次数 x 手工测试成本
自动化成本 = 脚本创建成本 + 维护次数 x 维护调试成本 + 脚本失败次数 x 脚本排错成本
在项目的迭代过程中,我对用例执行失败的具体原因进行了分析,总结出以下几点原因:
针对以上几点问题,我们测试框架和用例设计上进行合理优化后自动化的稳定性自然而然会提高,重点是我们需要在 UI 自动化前期阶段,对具体失败的原因进行分析,并且做出相应调整的动作,这样稳定性就会持续提高。
不管我们使用的是 Airtest 还是 Appium,在框架设计理念上是无区别的,当前框架包含以下几个特点:
import traceback
import allure
from appium.webdriver.extensions.applications import Applications
from common_utils.new_log import NewLog
from selenium.webdriver.support.wait import WebDriverWait
from config.project_common_config import ProjectConfig
from airtest.core.api import *
from ui_common_utils.wrapper_utils import wrapper_logger
class AppiumElementUtils:
log = NewLog(__name__)
logger = log.get_log()
@classmethod
@wrapper_logger
def select_locate_method(cls, driver, method_type, element_info):
"""目前元素的几种定位方式"""
if method_type == "id":
return driver.find_element_by_id(element_info)
elif method_type == "accessibility_id":
return driver.find_element_by_accessibility_id(element_info)
elif method_type == "xpath":
return driver.find_element_by_xpath(element_info)
elif method_type == "ios_predicate":
return driver.find_element_by_ios_predicate(element_info)
elif method_type == "ios_class_chain":
return driver.find_element_by_ios_class_chain(element_info)
elif method_type == "android_uiautomator":
return driver.find_elements_by_android_uiautomator(element_info)
elif method_type == "class_name":
return driver.find_element_by_class_name(element_info)
@classmethod
@wrapper_logger
def text(cls, text_concent):
"""
由于flutter-app安卓设备上,无法通过appium-send_keys进行文本输入
采用airtest中的text,输入文本
"""
text(text_concent)
@classmethod
@wrapper_logger
def check_element_status(cls, driver, element_info, method_type, wait_time):
"""判断元素状态,存在则返回元素,不存在则返回False"""
try:
element = WebDriverWait(driver, wait_time, 0.5).until(
lambda x: cls.select_locate_method(x, method_type, element_info))
cls.logger.info("找到元素【%s】" % element_info)
return element
except Exception:
err_msg = "未找到%s元素" % element_info
cls.save_screenshot(err_msg)
cls.logger.error("未找到%s元素", element_info, exc_info=1)
cls.logger.error("错误信息:\n%s" % traceback.format_exc())
return False
@classmethod
@wrapper_logger
def get_element(cls, driver, element_info, method_type="xpath", wait_time=4, is_raise=True):
"""
eg:
get_element(driver, "密码登录", "accessibility_id")
get_element(driver, "//android.widget.ImageView[5], "xpath")
"""
# 元素与元素点击之间间隔300ms,解决页面切换,元素点击失败的场景
time.sleep(0.3)
result = cls.check_element_status(driver, element_info, method_type, wait_time)
if result:
return result
else:
if is_raise:
cls.logger.error("未获取到元素,重启app")
cls.restart_and_check_up_app(driver)
raise Exception(("获取元素异常, element_info: %s" % element_info))
else:
cls.logger.info("获取元素异常,不重启app, element_info: %s" % element_info)
return None
@classmethod
@wrapper_logger
@allure.step("断言元素存在")
def assert_element_exists(cls, driver, element, wait_time=3, is_restart=True):
for i in range(wait_time):
page_source = driver.page_source
if element in page_source:
cls.logger.info("断言成功,找到[%s]元素" % element)
return True
else:
time.sleep(1)
cls.save_screenshot("断言失败,未找到[%s]元素" % element)
if is_restart:
cls.logger.info("断言失败,未找到[%s]元素, 重启app" % element)
cls.restart_and_check_up_app(driver)
return False
@classmethod
@wrapper_logger
def restart_and_check_up_app(cls, driver):
"""重启app, 根据项目判断进行重启后的操作"""
cls.restart_app(driver)
if ProjectConfig.project_name == "ytt":
from project_pages.ytt_actions.common_action import check_up_ytt_app
check_up_ytt_app(driver)
else:
pass
@classmethod
@wrapper_logger
def restart_app(cls, driver):
"""重启app, 根据项目判断进行重启后的操作"""
Applications.close_app(driver)
Applications.launch_app(driver)
class LoginPage:
"""登录页面"""
@classmethod
@wrapper_logger
@allure.step("勾选用户协议")
def click_user_agreement(cls, driver):
if driver.capabilities["platformName"] == "Android":
xpath = '//android.view.View[@content-desc="欢迎登录"]/following-sibling::android.view.View[1]'
else:
xpath = '//XCUIElementTypeStaticText[@name="欢迎登录"]/following-sibling::XCUIElementTypeOther[1]'
aeu.get_element(driver, xpath, "xpath").click()
@classmethod
@wrapper_logger
@allure.step("点击输入手机号")
def click_and_input_phone(cls, driver, android_phone, ios_phone, is_raise=True):
if driver.capabilities["platformName"] == "Android":
xpath = '//*[@text="请输入手机号"]'
aeu.get_element(driver, xpath, "xpath", is_raise=is_raise).click()
aeu.text(android_phone)
else:
element = 'label == "请输入手机号"'
aeu.get_element(driver, element, "ios_predicate", is_raise=is_raise).send_keys(ios_phone)
@classmethod
@wrapper_logger
@allure.step("点击获取验证码按钮")
def click_get_verification_code(cls, driver, is_raise=True):
aeu.get_element(driver, "获取手机验证码", "accessibility_id", is_raise=is_raise).click()
@wrapper_logger
def login(driver, android_phone, ios_phone, first=False):
"""
存在一键登录,则点击切换成验证码登录
点击安卓同意按钮
点击手机号输入框
输入手机号
点击获取验证码
输入验证码
"""
if first:
lp.click_agree_btn(driver)
exists_once_login(driver)
# 点击升级按钮
lp.click_upgrade_remind_btn(driver)
assert aeu.assert_element_exists(driver, "获取手机验证码", wait_time=3)
lp.click_user_agreement(driver)
lp.click_and_input_phone(driver, android_phone, ios_phone)
lp.click_get_verification_code(driver)
assert aeu.assert_element_exists(driver, "输入手机验证码")
lp.input_verification_code()
assert aeu.assert_not_element_exists(driver, "输入手机验证码")
@allure.feature("验证码登录")
class TestLogin:
log = NewLog(__name__)
logger = log.get_log()
@allure.story('已注册司机账号登录')
@allure.description("已设置密码,已注册司机账号登录")
@allure.severity(CommonConfig.Blocker)
@pytest.mark.run(order=ProjectConfig.case_order.get("test_login_1"))
@pytest.mark.v110
@pytest.mark.new
# 通过pytest进行单个用例调试时,打开下面注释
# @pytest.mark.parametrize("devices", aiu.get_devices_info(devices_type="android"))
def test_login_1(self, devices, driver):
# 验证码登录账号
android_phone = login_params["test"]["android_phone"]
ios_phone = login_params["test"]["ios_phone"]
# 登录权限处理
CommonElements.click_allow_locate(driver)
LoginPage.click_agree_btn(driver)
# app环境切换
switch_app_env_rc(driver)
# 登陆方法封装
login(driver, android_phone=android_phone, ios_phone=ios_phone)
# 登录成功校验
assert PasswordLoginPage.assert_login_success(driver)
下一篇介绍当前项目中使用的 UI 自动化框架