UI 自动化实践总结

为什么要做 UI 自动化?

说到 UI 自动化,有些人肯定会想,我们项目已有在接口自动化,为什么还要做 UI 自动化?

做 UI 自动化面临哪些问题?

提到 UI 自动化,大家就会提到:

下面我们通过讲如何做 UI 自动化来解决这 2 个问题。

怎么做 UI 自动化?

前面提到的 2 个问题相比接口自动化来说确实是存在的,我们需要做的是尽可能提高我们的 ROI 和稳定性

如何提高 ROI?

我们先看下 UI 自动化收益及成本的计算公式:

自动化收益 = 有效迭代次数 x 手工测试成本

自动化成本 = 脚本创建成本 + 维护次数 x 维护调试成本 + 脚本失败次数 x 脚本排错成本

如何提高自动化稳定性?

在项目的迭代过程中,我对用例执行失败的具体原因进行了分析,总结出以下几点原因:

针对以上几点问题,我们测试框架和用例设计上进行合理优化后自动化的稳定性自然而然会提高,重点是我们需要在 UI 自动化前期阶段,对具体失败的原因进行分析,并且做出相应调整的动作,这样稳定性就会持续提高。

UI 自动化框架设计要点

不管我们使用的是 Airtest 还是 Appium,在框架设计理念上是无区别的,当前框架包含以下几个特点:

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 自动化用例设计要点

登录用例参考

元素封装模块
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 自动化框架


↙↙↙阅读原文可查看相关链接,并与作者交流