大力智能学习灯 Auto-Shoots 框架设计

作者:大力智能QA团队 老胡

业务背景

大力智能作业灯拥有智能双摄设计,不在家也能帮家长远程辅导孩子作业,创新台灯形态结合大力智慧眼,专为孩子书桌作业场景设计,实现智能识别到讲解,帮助孩子提升作业效率。更可与家长端App“大力辅导”双端联动,实时学情同步,为学生和家长创造平衡且愉快的学习辅导关系。

产品特征

智能硬件 传统手机APP
被测对象 智能硬件
智能硬件+手机(家长端)交互
智能硬件+电脑(教师端) 交互
一般就是手机App
输入方式 语音输入
手势输入
图像输入(提搜/点读)
声纹识别
点击滑动为主
漏测成本 召回设备,成本极其高昂 可热修复解决
性能问题 关注App本身内存/CPU/帧率
关注系统的剩余内存/CPU/CPU温度
关注系统剩余磁盘空间
只需关注App本身
优先级并不会太高

功能测试难点

  • 业务高速更迭带来的case维护问题
    • 由于业务高速更迭,界面变化频次很高,由于当前界面校验比较严格,所以给老case的维护带来了巨大的压力。
  • 多模态输入
    • 传统的自动化测试都是模拟用户点击按键操作即可。而我们还需要处理语音输入数据,准备纸张来供设备拍摄。
  • 环境的维护
    • 由于有语音输入,所以环境要求没有噪音且多台设备无法放置在一处。
  • 技术更新&软件重构
    • 当前app采用webview、flutter、lynx等技术来渲染页面,而这些页面的元素是无法用native的方法去获得的。
  • 多应用&多端检测
    • 由于项目内容是多个应用之间的交互,所以操作检查需要在智能硬件和手机上同步进行,难度加大。
    • 智能硬件上有多个App在运行,测试过程中需要在app之间来回切换。所以给框架设计带来难度。

当前收益:

公司的Shoots自动化框架非常给力,功能强大。但在实际使用Shoots中,结合业务的实际需求,我们认为UI自动化还应具有以下能力:

  1. 丰富测试手段,提升能力找到更多问题
  2. 降低编写用例的成本
  3. 降低消费测试结果的成本
  4. 能一份代码兼容多个版本的测试从而能分支巡检,做到测试前置
  5. 能收集“人工难测”的数据来提Bug

总体概述

整个框架围绕以下几个大的功能点进行支持

  1. 丰富测试手段,提升能力找到更多问题
  2. 降低编写用例的成本
  3. 降低消费测试结果的成本,做到数据跟踪和统计,趋势判断
  4. 能一份代码兼容多个版本的测试从而能分支巡检,做到测试前置
  5. 能收集“人工难测”的数据来提Bug

用例编写:降低成本

自动化脚本生成

传统的Shoots写法:

红框处的Window是需要取名,而且对应方法也需要取合适的函数名。这些导致其实是无法做到自动生成的。

下面是自动生成的代码举例:类名直接退化为对应的中文名(录制过程中手动输入),对应的函数点击也会以id名+文字注释的方式自动给出。

def auto(self):
    self.wait(5)
    self.step("开始复现脚本,请尽快重命名") #todo 需要重命名
    if not self.checkEnterWin('主界面'): return False 
    self.touchOcrKey('作业批改')
    if not self.checkEnterWin('拍照\_主界面'): return False 
    self.tapKey('单题搜')
    self.tapKey('take\_photo')  # text = 单题搜
    if not self.checkEnterWin('拍照框题页'): return False 
    self.tapKey('cropper\_iv\_retake')  # text = 整页检查
    if not self.checkEnterWin('拍照\_主界面'): return False 
    self.tapKey('iv\_flashlight')  # text = 补光
    self.tapKey('iv\_flashlight')  # text = 补光
    self.tapKey('iv\_close')
    if not self.checkEnterWin('主界面'): return False 
    return True
复制代码

自动生成页面快照

case模块化设计

当我们写了较多用例后,就会发现很多逻辑其实都是原子操作,又或者一组操作经常被用到。如果我们让其存在于多个地方,一旦发生需求变更又或者脚本有问题,就要到处救火。而且写case的成本也会提升。基于这个考虑,我们提出了一种将一组操作原子化的能力。

要求每组操作都能起合适的名字,篇幅也不会太长。

自定义弹窗消除

现在由于各种推荐+推送所以在各个场景下都会不受控制的出现一些推荐位窗口。而这些窗口就非常难以处理。

基于这个现象,这里特地做了优化,在各个环节进行了推荐位窗口的处理。

POP\_WINDOWS = {
    #'学生端\_坐姿检测\_结果' : \['bt\_pose\_debug'\],
    #'学生端\_坐姿检测\_结果' : \['btn\_start', 2, 'KEYCODE\_BACK', 1, 'bt\_pose\_debug', 1\],
    '学生端\_坐姿检测\_结果' : \['btn\_start\_finish', 1, 'KEYCODE\_BACK', 0.5\],
    '学生端\_作业模式\_作业提醒' : \['iv\_close', 1\],
    '学生端\_准备升级' : \['update\_reboot\_later', 1\],
    '学生端\_作业开始提示' : \['iv\_close', 1\],
    '勋章\_获得' : \[\[320, 1300\], 1\],
}
复制代码
def dealWithPopWindows(self, win):
    popWindows = self.getPopWindows()
    if popWindows == None or len(popWindows) == 0: return win
    for k,v in popWindows.items():
        if window\_manager().isWindow(win, k):
            for item in v:
                if type(item) == int or type(item) == float:
                    self.wait(item)
                elif type(item) == list and len(item) == 2:
                    self.mainDevice().touch(item)
                elif item.startswith('KEYCODE'):
                    Log.warning("发现Pop窗口\[{}\],尝试通过按Key \[{}\]关闭".format(k, item))
                    self.mainDevice().keyEvent(item)
                else:
                    Log.warning("发现Pop窗口\[{}\],尝试通过按键\[{}\]关闭".format(k, item))
                    win.clickItem(item)
                    self.wait(1)
            # 这里必须用递归,因为存在这样的场景。假定循环顺序是ABCD,C处理完了变成A了,但是A已经循环过了.还有一种情况就是点击按钮并没有成功关闭,那么用递归也能重复检测这个界面。所以用递归重新循环会更保险
            return self.dealWithPopWindows(self.refreshWin())
    return win
复制代码

也就是写代码时只需要在常量中定义好弹窗信息文件以及退出方法(支持多步骤退出),写代码时完全不用考虑这些细节,因为全部都封装好了。

用例编写:丰富手段来找到更多问题

自动推荐页面校验元素

和服务端的Response json树一样,Android的显示页面也是View树结构。所以和服务端结果校验一样,页面校验也是对于页面树的校验。

思路是评估页面树每个节点的价值。android里面有容器View,ButtonView,TextView。根据业务经验,我们一般会更关注ButtonView(可点击属性)以及TextView(是否有文本属性)。而容器View中,如果是ListView或者RecycleView,我们会通过判断里面ViewID是否相同来推断其是否是实时生成的。如果是实时生成的,那么校验价值就非常弱。

同样我们还会从显示区域大小这个维度来判断一个节点是否有校验价值。显示区域过大又或过小的View被校验的价值都是很低的。

图搜/文字搜

支持检测关键字是否存在(可多个一起检查,支持模糊匹配)。

也可直接点击:

还可在一段时间内反复尝试,针对那种页面加载时间不确定的场景。

多设备协同操作

组件支持多设备

case中的实际运用

放音/录音/录制视频

借助辅助设备的能力

  • 放音(语音测试)
  • 录视频(视频分帧方案)
  • 录音(通过检测波纹来判断是否真实有声音出来,通过asr来识别放出的音是否符合预期)

音频Mock文件灌入/广播模拟

两者都需研发配合,做到这点后可以降低对环境的依赖。我们关于音频测试所经历的阶段是:

  1. MAC放音
  2. 辅助机放音

因为上bytest后电脑无法放音,只能用辅助机放音。缺点是非常费环境,需要隔音设备。当前这种模式并没有完全淘汰,因为如果要模拟真实场景还得辅助机放音。

  1. 广播模拟

比较好的方式。通过广播文字,广播的文字等价于(语音asr的文本结果)。缺点在于和真实的业务流程还有些差距,会导致部分埋点丢失。需要研发配合。

  1. 语音文件灌入

直接用语音文件灌入,流程和真实场景基本吻合。一样需要研发配合。

图片Mock

我们一开始通过辅助设备(PAD)来放图。但后续效果并不是太好,所以使用纸张代替。但纸张无法动态切换,最终我们和研发商量了一种mock方案,可直接用我们推送的文件来mock拍照的结果。

用例更新维护

自动兼容AB实验

页面跳转逻辑的变迁。

版本A:A->B->C->D

版本B:A->B->D

如果要做到多个版本都能跑自动化脚本,那么必须做到流程差异兼容。

同样如果需要做AB试验,只要配置相关的页面信息,那么也可以一份代码搞定。

解决的问题:

1:解决AB试验

A方案:

entrance -> service_ui_A -> service_ui_A_1 -> end_page

B方案:

entrance -> service_ui_B -> service_ui_B_1 -> service_ui_B_2 -> end_page

if helper.judgeAndAction({
    'entrance' : lambda win: helper.tapKey('enter'),
    'service\_ui\_A' : lambda win: helper.tapKey('enter\_a'),
    'service\_ui\_A\_1' : lambda win: helper.tapKey('enter\_a1'),
    'service\_ui\_B' : lambda win: helper.tapKey('enter\_b'),
    'service\_ui\_B\_1' : lambda win: helper.tapKey('enter\_b1'),
    'service\_ui\_B\_2' : lambda win: helper.tapKey('enter\_b2'),
    'end\_page' : None, #出口页面
}, '切换到主界面', timeToWait=20, timeWaitPerTime=0.5, actionWhenNoMatch=lambda: exitPage(helper)):
复制代码

2:有些页面完成任务后会出现你获得了XX勋章。但是又不是每次都出来,所以这个时候就要

if not self.judgeAndAction({
    '勋章\_获得' : lambda win: self.tapKey('submit'),
    '作业模式\_全部完成' : None,
}, '完成所有作业'): return False
复制代码

3:有些结果页依赖于外部媒介以及内部算法。比如检查作业,检查的内容可能能识别可能不能识别。此时就需要对可能的结果页进行枚举和处理。

return self.judgeAndAction({
    '检查作业\_无法检查' : lambda win: self.tapKey('btn\_right'),
    '检查作业\_有结果' : lambda win: self.tapKey('btn\_right'),
    '检查作业\_对答案中': lambda win: self.tapKey('btn\_right'),
    '检查作业\_全对' : lambda win: self.tapKey('homework\_close'),
    '检查作业\_对答案\_结束': lambda win: self.tapKey('homework\_correct\_close'),
    #'检查作业\_对答案\_结束\_225': lambda win: self.tapKey('homework\_correct\_close'),
    '检查作业\_对答案\_计算题' : lambda win: self.tapKey('btn\_next'),
    'Launcher\_下半部': None,
    'Launcher' : None,
}, '检查作业', timeToWait = 60)
复制代码

4:为了确保回到某个界面。然后通过定义actionWhenNoMatch来尝试回到一个新页面

比如下面这个例子就是通过不断按已知的退出按钮来达到任意界面返回到主界面。

if helper.judgeAndAction({
    'Launcher' : lambda win: swipe\_X(helper.mainDevice(), False),
    'Launcher\_下半部' : lambda win: swipe\_X(helper.mainDevice(), False),
    '学生端\_夜间模式': lambda win: dealWithNightMode(helper, win),
    '主界面\_个人定制' : lambda win: swipe\_X(helper.mainDevice(), True),
    '学生端\_视频通话\_对话简约': lambda win: helper.tapKey('control\_container'),
    '学生端\_视频通话\_对话中': lambda win: helper.tapKey('buttonCallCancel'),
    '学生端\_主界面' : None,
}, '切换到主界面', timeToWait=20, timeWaitPerTime=0.5, actionWhenNoMatch=lambda: exitPage(helper)):
复制代码

在手机端上就更简单了,只需要按返回键就可以了

def backToMain(helper):
    return helper.judgeAndAction({
        '主界面': None,
        '主界面\_神灯\_已登录': lambda win: helper.tapKey('首页'),
        '主界面\_我的\_未登录': lambda win: helper.tapKey('首页'),
        '主界面\_我的\_已登录': lambda win: helper.tapKey('首页'),
        '主页面\_学情': lambda win: helper.tapKey('首页'),
        '家长端\_视频通话\_确认类型': lambda win: helper.tapKey('plugin\_tv\_cancel'),
        '家长端\_视频通话\_主叫': lambda win: helper.tapKey('tvCancelBtn'),
        '家长端\_视频通话\_来电': lambda win: helper.tapKey('tvRefuseBtn'),
    }, '回到家长端主页',
        actionWhenNoMatch=lambda: helper.mainDevice().press\_back()
    )
复制代码

多版本页面兼容

简单来说就是可以写多个窗口,然后在运行时自动匹配到最合适的那个

当进行任何 [单页面/多页面] 校验后,都会尝试去将页面和对应的版本号绑定。

用例结果消费

输出当前页和预期页面快照的差异报告

判断当前页面与历史页面快照是否匹配

当我们用例失败时,利用原生shoots,我们只能知道哪个按键失败了。而实际上如果我们想要自动归因,或者分析时有更多的信息,那么我们会很希望知道当前是哪个页面。

当然我们也可以通过故障截图来获取当前页面,但是有的时候页面和我们想的一样,但可能相关按键不在或改名了,就会给我们定位带来困难。

综上,我们很需要在失败时输出更多的信息来帮助我们定位问题自动归因。

这里被识别为多个结果,这是按照匹配度从高到低排列。所以最后认定当前页面是学生端_主界面_255。

错误分析自动生成/自动开Bug

为了减少结果消费的人力成本,所以我们尽量采用自动消费的方式。我们将用例+步骤+错误原因+当前页面+(期望页面|按键|期望图像识别到的文字)等生成核心错误原因。

用这个核心错误原因可自动对结果进行归因。如果关键字和之前提过故障的关键字一样,所以被自动归因为【重复BUG】。另外会附上当前故障的状态。如果已经解决可关注时间,对应合入的分支。

失败场景还原

当用例失败后收集足够的信息,达到故障场景99%还原。

  • 每一步的截图
  • 过程操作(点击的按键,检查通过的页面,图像识别点击的真实坐标)
  • RD要求的定向日志
  • 过程中系统的日志以及错误level日志

所以一旦出现问题,我们不需要马上去看,对于消费的实时性要求不高,不至于打断研发和测试的工作节奏。

日志收集系统

配置日志关键字+输出文件。则测试过程中会自动收集日志。

key为文件名,value为关键字,支持多关键字,只要匹配任何一个,都会写入。

这个主要用于专项查问题以及后面会提到的StrictMode故障收集。

自动诊断

当前自动诊断以多种形式进行

  1. 插件

  1. 用例运行时检测

  2. 根据测试结果进行检测

这种一般是根据日志进行检测。有可能是Shoots的,也可能是自定义的。

消费结果可度量

和消费结果的同学对齐消费标准。消费过程中注明处理的时间,由于刚处理完,时间估算的误差理论上很小。这样一来,运行一段时间后便可以看出整个过程中测试结果消费的时间明细。

业界都认为UI自动化ROI很低,业务同学的体感也很差。那么实际效果如何呢?这个数据的产出实际上对传统观念是一种认知纠偏,也对自动化ROI的评估提供了准确数据。通过这些报告,可以准确度量出测试结果消费、因为研发的改动而更新脚本的开销、脚本不完善的开销,方便我们计算客户端自动化的实际收益。

如果测试结果没有及时消费则会有告警

测试左移

分支巡检

将测试前置!前置!再前置!

在研发把分支拉出后,就开始打包进行自动化测试。随后一旦研发有修改,我们检测到GIT commit信息发生变化就会打包测试。在这样的操作下,对于分支的质量我们是有感知的,可以前置风险。

人工难测

性能数据收集

会在测试阶段收集性能数据,并自动进行判断是否有内存泄露,或者对一些阈值进行告警

算法验收

有一些算法上的改动需要端到端进行压测验收。此时就需要用到之前的图片/音频Mock的能力来切换被测资源,然后再进行测试,达到验收效果。

未来展望

UI自动化还是很难做为完全无人测试。而且研发修改了页面,测试也得跟进。很难做到真正的自适应。个人认为未来UI自动化的发展会在两个方向上继续:

1、精细化UI校验

通过对UI控件采样+截图+布局记忆+人工断言托底能取代大部分人工。

2、智能遍历+自动断言

历所有的UI控件,包含新增页面。能做到自动断言,自动甄别问题来提交人工审核

掘金尾部官号.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享