通用技术 UI 自动化的稳定性和效率

QE LAB for QE LAB · 2023年06月16日 · 最后由 Soner 回复于 2024年03月14日 · 5333 次阅读

作者:张婷 | QE_LAB , 陈佩|TW QA 咨询师

提起 UI 自动化测试,一定绕不开这两个很重要的话题:稳定性和效率。围绕这两个方面,下面介绍下基于 python+appium+pytest+allure 这套 “组合拳"的一些实践和方案。

稳定性

1.PO 是基础

PO(Page Object)的核心思想是分层:

  • BasePage 层封装了页面元素的基本操作,比如查找元素、获取元素属性、执行元素操作等;
  • PageObject 层分别封装了各个页面的所有元素和操作;
  • TestCase 层是偏向自然语言的测试用例,将测试用例与页面元素和操作分离的同时增强用例的可读性。

可以确认的是,清晰简洁的分层可以提升脚本可读性、可维护性和可复用性,而对各层的定制封装使其各司其职,也是在保证执行的稳定性。

结合 appium+pytest+allure 这套组合拳,这个架子就像这样:

2.等待稳定

因为页面加载时间存在变数,元素查找结果也变得随机,直接导致用例执行不稳定。而我们的每个业务场景中步骤(用例)都不少且又存在依赖关系,fail 出现的位置直接决定通过率的高低,所以每次运行用例,都得默默祈祷🙏,通过率忽高忽低成了玄学。

为解决这个问题,我们首先是增加了⼀些操作的异常处理,例如可以在点击操作后判断预期元素是否出现/消失,若不符合预期则等待 1-2s 后再次重复点击。

后来发现可以定位到 loading 图标,于是在查找上有增加一层 “等待 loading 图标消失",再配以其他隐式等待和显式等待,通过率确有提升。

3.定制的 rerun

手工测试遇到问题时,我们是会多试几次的,如果是偶尔的系统不稳定,一般重试可以通过则认为 “通过"。我们也为自动化测试 case 设置了 rerun,使用到的是 pytest 的插件 pytest-rerunfailures:

@pytest.mark.flaky(reruns=1)

这样的设置会在 case 执行失败时从它的第一行代码再来执行一次,但通常这时所在页面已不是上个 case 的结束页面,步骤很难续上(case 间是有依赖关系的),这样的 rerun 效果并不好,还需要做些定制:

  • 每个 case 执行异常/失败后都重启 app,将页面恢复到同一个初始页——给 case 加上装饰器;
  • 每个 case 的⼊⼝进⾏适配处理,不仅可从上个 case 的结束⻚⾯开始执⾏,也能从 app 的初始页开始执⾏。

这样的定制有了后,不仅每个 case 可以按需 rerun,case 还可以单独运行调试了,节省脚本开发时间。

4.还有优雅的 retry

TestCase 层有了 rerun,PageObject 和 BasePage 也可以有,python 有些现成的第三方库比如 tenacity:

from tenacity import retry, stop_after_attempt


@retry(stop=stop_after_attempt(3))
def test_retry():
    print("等待重试")
    raise Exception


test_retry()

这样的 retry 现阶段虽能解决一些问题,但这不是长久之计,更应该做的是找到失败的根因,然后解决它。

效率

1.多设备运行

我们的一个业务场景执行时间大约在 10 分钟左右,十几个场景串行执行时间就在 2~3 小时左右,多设备运行是提升效率最直接的方案,我们尝试了使用 appium2.0,结果是令人惊喜的,后面计划进行实践。

首先来看看 appium2.0 的一些变化:

  • 曾经的 appium 作为跨平台的杰出代表,集成了各个平台的驱动,appium2.0 将这些驱动与本体解藕,可以按需来安装;
  • 支持个人修改和定制已有的驱动,甚至自己做一个完全新的驱动进行测试;
  • 插件模式,比如有基于图形识别的定位和 diff 机制。

插件 appium-device-farm 是实现多设备运行的主角,它的主要功能是测试设备的管理分发,并发的事情交给 client 来做。

测试设备的管理分发

1.appium 要使用 npm 安装,先安装 node.js,然后

npm install -g appium@next

2.使用 appium CLI 安装 driver:

appium driver install uiautomator

3.使用 appium CLI 安装插件

appium plugin install --source=npm appium-device-farm
appium plugin install --source=npm appium-dashboard

4.插件模式启动

appium server -ka 800 --use-plugins=device-farm --config ./config.json -pa /wd/hub

浏览器打开http://127.0.0.1:config 中设置的 port}/device-farm/就可以看到本机连接到的移动设备啦{

case 并发

5.测试脚本中将 Appium 测试执行 URL 指向插件启动设置的地址

webdriver.Remote('http://127.0.0.1:{config中设置的port}/wd/hub', caps)

6.脚本安装 pytest 插件 pytest-xdist,运行 pytest 时加上参数-n=2、--dist=loadfile 就可以同时拉起两个设备分别执行两个场景中的 case 了,

pytest.main (['-v', '-s', '--dist=loadfile', '-n=2',  test_case_path])

(以上更多内容请移步官网:https://appium.io/docs/en/2.0/

2.巧⽤ dependency

原则上编写⽤例需要注意⽤例之间的独⽴性,但实际测试时我们经常发现部分⽤例之间确实存在关联,⽆法做到彻底独⽴。

例如,待测场景为串⾏的业务流,前⼀⽤例若执⾏失败就会影响后续⽤例的正常执⾏。

为了不做⽆⽤功,我们使⽤插件 pytest-dependency 来设置⽤例之间的依赖关系:

import pytest


class TestDemo:
    # 通过装饰器标记当前⽤例为被依赖⽤例,被依赖⽤例需要优先关联⽤例执⾏
    @pytest.mark.dependency()
    def test_01(self):
        assert 1 == 2

    # 通过使⽤装饰器关联被依赖⽤例,通过depends参数指定⽤例名称关联⽤例,depends参数可以关联多个 测试⽤例,使⽤“,”分隔即可
    @pytest.mark.dependency(depends=['test_01'])
    def test_02(self):
        print("测试⽤例02,跳过")

当⽤例 B 依赖于⽤例 A 时,若⽤例 A 执⾏失败,则⽤例 B 将会⾃动跳过不执⾏。如此,就可以避免去执⾏⼀个必定会失败的⽤例,节省运行时间,提升效率。

3.集成 requests

我们在 appium+pytest+allure 的 UI 测试框架下还集成了 requests,⽤ API 代替 UI 操作来实现依赖场景数据的构造和部分数据流转。参考 PageObject,api 请求也按照功能模块封装为了 ApiObject。

如此设计相⽐通过全程 UI 操作具有下述优势:

  • 提⾼测试执⾏效率
  • 避免测试在⾮⽤例验证点的地⽅提前报错
  • 可以验证数据一致性

总结

在 UI 自动化测试中,提高稳定性和效率是至关重要的。除了以上这些实践外,合理的元素定位、适当的等待时间、优化运行平台和设备也是必不可少的。最重要的是选择适合项目的方式和方法,持续学习和改进。

共收到 2 条回复 时间 点赞

appium2.5.1 配合 pytest-xdist,请教下 config.json 和 case 中的 caps 是如何配置的 还需要添加设备信息吗?按照我自己的设置后,有以下几种情况

  1. 只有一台能起来,最后都失败
  2. 两台都起来,但最后都失败 报错信息
FAILED testcase/test_demo.py::test1 - selenium.common.exceptions.NoSuchElementException: Message: An element could not be located on the page using the given search parameters.; For docu...
FAILED testcase/test_demo1.py::test2 - selenium.common.exceptions.UnknownMethodException: Message: The requested resource could not be found, or a request was received using an HTTP metho...

config.json

{
  "server": {
    "port": 31337,
    "plugin": {
      "device-farm": {
        "platform": "android",
        "skipChromeDownload": true
      }
    }
  }
}

case1 示例

caps = {
    "platformName": "Android",
    "automationName": "UiAutomator2",
    "appActivity": "xxx",
    "appPackage": "xxx",
    "autoGrantPermissions": True,
    "unicodeKeyboard": True,
    "resetKeyboard": True,
    "newCommandTimeout": 0,
}
url = "http://127.0.0.1:31337/wd/hub"
capabilities_options = UiAutomator2Options().load_capabilities(caps)


def test1():
    driver = webdriver.Remote(command_executor=url, options=capabilities_options)
    print(f"我是test1:{id(driver)}")
    driver.implicitly_wait(10)
    driver.find_element(
        "xpath",
        "(//xxx)[2]",
    ).click()

    print(f"我是test1:{id(driver)}")
    driver.close()
    driver.quit()

case2 示例

caps = {
    "platformName": "Android",
    "automationName": "UiAutomator2",
    "appActivity": "xxx",
    "appPackage": "xxx",
    "autoGrantPermissions": True,
    "unicodeKeyboard": True,
    "resetKeyboard": True,
    "newCommandTimeout": 0,
}
url = "http://127.0.0.1:31337/wd/hub"
capabilities_options = UiAutomator2Options().load_capabilities(caps)


def test2():
    driver = webdriver.Remote(command_executor=url, options=capabilities_options)
    print(f"我是test2:{id(driver)}")
    driver.implicitly_wait(10)
    driver.find_element(
        "xpath",
        "(//xxx)[3]",
    ).click()

    print(f"我是test2:{id(driver)}")
    driver.close()
    driver.quit()

我把 caps 里的 noreset 参数去掉,可以正常分配设备了

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册