UI 自动化实践总结
为什么要做 UI 自动化?
说到 UI 自动化,有些人肯定会想,我们项目已有在接口自动化,为什么还要做 UI 自动化?
- 我们先看看 UI 自动化中 UI 的含义:UI 即 User Interface(用户界面)的简称,
UI 自动化测试做的事情就是模拟用户行为进行操作,还原用户使用场景,UI 自动化能够帮助我们确保线上不出现 P0 级别的问题,比如登录不成功,页面打不开等等。这是接口自动化无法比拟的, 也体现了 UI 自动化的价值。 - 每更新一个迭代版本,在有接口自动化的基础上,我们还是需要手工对历史功能进行回归,随着功能的迭代,手工回归成本逐渐增高;此时如果可以使用UI 自动化去代替人工进行回归,就可以降低人力回归成本;回归的次数越多,UI 自动化的价值就越高。
- 当客户端开发升级一个公共组件时,我们要进行全量回归及兼容性测试,接口自动化就显得有些 “爱莫能助”,此时就体现了 UI 自动化的重要性,在业务没有改动的情况下,我们可以通过 UI 自动化进行回归测试和兼容性测试。
做 UI 自动化面临哪些问题?
提到 UI 自动化,大家就会提到:文章源自玩技e族-https://www.playezu.com/187914.html
- UI 自动化维护成本高,ROI 低
- 做过 UI 自动化的同学肯定都会遇到这个问题,辛辛苦苦写好的测试用例,跑了还没几天,新的需求之后,开发把页面改了,原来定位的控件失效了,N 条测试用例跑不通了。每条用例改完之后,UI 又变了,又得改,很奔溃有没有。回想在货运项目中,一个新增车辆的 UI 就改了 4、5 个版本;
- 做过 UI 自动化的同学肯定都会遇到这个问题,辛辛苦苦写好的测试用例,跑了还没几天,新的需求之后,开发把页面改了,原来定位的控件失效了,N 条测试用例跑不通了。每条用例改完之后,UI 又变了,又得改,很奔溃有没有。回想在货运项目中,一个新增车辆的 UI 就改了 4、5 个版本;
- UI 自动化稳定性差,这次执行成功了,下次就失败了,排查问题还耗费时间
- 测试用例本地调试的时候是通的,怎么批量执行的时候就失败了。。。
- 这个用例上次跑通了,怎么这次又失败了。。。
- app 界面上明明有这个元素,怎么又定位不到了
下面我们通过讲如何做 UI 自动化来解决这 2 个问题。文章源自玩技e族-https://www.playezu.com/187914.html
怎么做 UI 自动化?
前面提到的 2 个问题相比接口自动化来说确实是存在的,我们需要做的是尽可能提高我们的 ROI 和稳定性。 文章源自玩技e族-https://www.playezu.com/187914.html
如何提高 ROI?
我们先看下 UI 自动化收益及成本的计算公式: 文章源自玩技e族-https://www.playezu.com/187914.html
自动化收益 = 有效迭代次数 x 手工测试成本文章源自玩技e族-https://www.playezu.com/187914.html
自动化成本 = 脚本创建成本 + 维护次数 x 维护调试成本 + 脚本失败次数 x 脚本排错成本文章源自玩技e族-https://www.playezu.com/187914.html
- 开发 UI 用例之前,我们根据上诉公式评估 ROI,评估自动化成本是否小于手工测试成本;当我们知道产品已经有了对该 UI 改动的计划,那我们暂时可以不需要对此功能进行开发,在项目初期,这种情况经常发生,所以选取相对稳定的功能进行 UI 自动化用例开发是提高 ROI 的最有效手段;
- 针对 APP 来说,我们可以先选取验证码登录、密码登录、修改密码、修改个人信息这种基础功能,进行 UI 自动化覆盖。其次,在核心业务中,选取相对稳定的功能进行 UI 自动化覆盖;
- 在 APP 的迭代过程中我们无法避免 UI 上的改动造成的自动化维护成本的提高,我们需要做的是优化 UI 自动化框架,在 UI 发生变动后,可以快速的在历史用例的基础上修改成新的用例;(后文会介绍用例设计技巧)
- 在 UI 自动化进行一段时间后,进行成本分析,分析出最耗时的方面,再针对业务特性进行优化。
如何提高自动化稳定性?
在项目的迭代过程中,我对用例执行失败的具体原因进行了分析,总结出以下几点原因:文章源自玩技e族-https://www.playezu.com/187914.html
- 页面切换/控件加载场景下,元素定位太快,导致点击失效;
- 大家都以为 UI 自动化只会因为执行慢找不到元素,没想到有一天我竟然遇到了,因为页面切换太快,元素还没加载到指定位置 appium 就发现了它并且点击,结果当然会导致用例失败了
- 不同网络情况下,接口返回时间不一致,客户端一直 loading,导致元素存在但无法点击;看到这个 loading 我就感到很懊恼,想把开发打一顿!!
- 系统通知影响元素定位,导致元素无法点击;
- 直接关闭系统通知是最简单的办法,关闭后再没有因为系统通知导致用例失败
- 测试数据、前置数据存在问题,导致用例失败;
- 突然有一天登录用例失败了,吓得我赶紧去看下怎么登录还失败了,最后发现我的测试账号竟然被开发用了,打他们 ;
- 自动化的账号一般要单独维护,并且最好备注下,以免被其他人用了。
- 上一条用例失败了,停留在当前页面,导致下一条用例也执行失败了;
- 图像识别不稳定,这台设备识别成功了,那台设备又失败了(在货运项目中,我并没有采用图像识别,因为在同频项目实践过程中发现,图像识别并不稳定,我们应该尽量减少图像识别的使用)
针对以上几点问题,我们测试框架和用例设计上进行合理优化后自动化的稳定性自然而然会提高,重点是我们需要在 UI 自动化前期阶段,对具体失败的原因进行分析,并且做出相应调整的动作,这样稳定性就会持续提高。文章源自玩技e族-https://www.playezu.com/187914.html
UI 自动化框架设计要点
不管我们使用的是 Airtest 还是 Appium,在框架设计理念上是无区别的,当前框架包含以下几个特点:文章源自玩技e族-https://www.playezu.com/187914.html
- 对 appium/webdriver 底层操作进行封装;提高用例执行稳定性;
- 元素获取失败 or 断言失败后,需要进行 app 重启操作,避免影响其他用例执行;
- 采用 Page Object 设计模式,对页面元素、通用元素、通用操作进行封装,减少代码冗余、将业务和实现分离、 降低代码维护成本;
- 对通用元素的封装,在用例的维护过程中起着很关键的作用;试想当开发修改了一个返回元素后,你需要在 N 个用例中一个一个修改、调试是多个痛苦,所以元素的封装是很有必要的,当一个元素修改后,我们不需要修改用例,只需要更改此元素的封装方法即可。
- 对测试数据进行单独维护,降低后续维护成本;
- 有一天你发现你的 UI 用例失败了,原因是你登录的账号信息,被其他人修改了,你发现你的这个账号,在 N 个用例中都有使用,瞬间崩溃;如果在最开始设计的时候你的测试数据是单独维护的就不存在此问题,你只需修改测试数据里面的账号即可。
- 结合接口,实现部分功能,简化 UI 自动化流程;
- 在一些情况下可以利用接口,来简化测试流程;比如我们想设计装货用例, 如果从接单开始设计用例,提高用例执行时长不说,还极有可能因为接单失败,导致未验证到装货功能;此时我们可以直接通过接口实现,创建运单,直接进行装货功能的验证,简化 UI 执行流程。
- 完整的测试报告,包含用例步骤、日志、失败截图、操作录屏,快速排查定位失败原因;
- UI 用例的失败原因有很多,最开始 UI 自动化报告只包含失败截图,无法判断具体失败原因,增加录屏后,我们可以快速定位失败原因。
- UI 用例的失败原因有很多,最开始 UI 自动化报告只包含失败截图,无法判断具体失败原因,增加录屏后,我们可以快速定位失败原因。
- 增加元素自动解析模块;
- 通过成本分析得出 UI 自动化最耗费时间的是一个一个元素进行定位、所以增加元素自动解析模块,可以提高我们的开发效率 ;
- 通过成本分析得出 UI 自动化最耗费时间的是一个一个元素进行定位、所以增加元素自动解析模块,可以提高我们的开发效率 ;
- 直接通过 pycharm 进行用例调试;快速的脚本调试 ,也是减少自动化维护成本的一方面。
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)
UI 自动化用例设计要点
- 元素与用例分离;
- 尽可能封装通用操作;
- 如:登录、退出、返回、拍照、相册选择这些基础功能,这些高频操作均可进行封装;
- 保证用例的独立性,用例与用例之间不要有关联关系,这样不会因为 A 用例失败,引起 B 用例也失败,这样能提升用例的稳定性;
- 如:所有用例执行后都返回首页,失败用例自动重启也返回首页;用例都从首页开始设计
- 前置操作 or 后置操作能不用 UI 操作尽量不用 UI 操作;可以通过接口 or 操作数据库实现来简化执行流程;
- 脚本中尽量不使用坐标和图像识别;
- 除非实在没有办法的情况下,我建议用坐标比图像识别好一些,根据不同设备标记不同坐标,往往只是一次性的开发,但是图像识别,总是存在失败的现象,排查失败原因,也同样增加了 UI 的成本;
- UI 自动化是为了回归测试,而不是发现 bug,不要过多的去验证 UI 的正确性,这样会降低自动化的稳定性,也就降低了 UI 自动化的 ROI。
- 登录功能,我们只要验证点击登录进入首页,就说明登录成功了。
- 新增车辆功能,在新增车辆后,验证列表已存在新增的车牌号及车辆信息即可;
登录用例参考
元素封装模块
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 自动化框架文章源自玩技e族-https://www.playezu.com/187914.html
软件项目功能测试
评论