崔宇
2018年加入去哪儿网,目前担任机票客户端开发负责人,擅长ios、android以及RN技术,主导了 TARS-UI自动化测试系统的开发和落地使用。一、前言
去哪儿旅行 UI 自动化系统 TARS 于 2020 年 3 月启动,到 2020 年底达成一期预计目标。覆盖公共、机票、酒店等多个部门接入,月版发版30多次,回归执行自动化上百次,200多个项目发布执行 400 多次,发版无 P1、P2 故障,节省人力近千 pd 。
持续运行一段时间后,TARS 团队收集了运行中的各种问题,并制定解决方案,启动了新一轮的迭代设计。二、TARS 核心指标和问题
要洞察到 TARS 系统有哪些问题,首先要有一个合理的评价标准。在 TARS 系统运行过程中,我们总结了五个主要指标来体现 UI 自动化的执行情况。- 包括业务线覆盖度与 case 覆盖度。
- 业务线覆盖度代表是否覆盖了公司所有业务线。
- case 覆盖度指每个业务线回归 case 的覆盖程度。- 我们对前端项目的发版进行了 UI 自动化拦截,必须通过自动化才能发布。
- 在某些情况下,如果不能跑自动化,可以申请跳过。
- 拦截跳过率代表发布项目中没有跑自动化任务的比例。综合反应了 TARS 系统是否好用。假如一个自动化任务跑 100 个 case,通过的有 90 个,未通过的有 10 个。但未通过的10个不一定都是bug导致的,可能有 5 个是 case 不完善导致的,有 4 个是系统不稳定导致的,只有 1 个是真正的 bug,那么就认为准确率为 91% 。假如要发布的项目有 10 个 bug,通过自动化测试测出来 3 个,那么召回率就是 30% 。
通过对这些核心指标的持续关注,我们发现了以下几个问题:1. 覆盖度低,新业务线接入很少,case 更新也不频繁。分析原因,主要是因为老的系统接入成本太高,需要 QA 掌握很多 UI 自动化运行环境的知识。而且 case 编写成本比较高,大家添加新 case 的意愿比较低。case 覆盖度低的话,那么就不能覆盖足够多的业务场景,bug 召回率就会降低。
2. 拦截跳过率高。分析原因,是因为每一次自动化任务执行时间比较长,而且由于系统原因造成的 case 失败率高,经常误报不通过。操作体验也比较差,同学们为了尽快发布,经常会选择跳过自动化任务。
3. 准确率不到80%。分析原因有几个方面的问题。首先是,使用线上数据进行测试,由于业务一直在迭代,使用线上数据经常会遇到 case 没有覆盖到的操作,比如新弹窗等。第二,人工编写的 case质量参差不齐,这也决定了 case 是否健壮稳定。第三,自动化框架不稳定经常会造成手机控制失败,case 运行不起来等问题导致 case 执行失败。
4.每个任务运行时间比较长。分析原因,case失败导致重试的次数多以及设备没有充分被利用是主要原因。
如何解决以上问题,就是我们本次重构的关键。总结下来,我们要解决的问题如下:- case编写难度高,线上数据变化多,case 不容易覆盖各种情况;
平台操作体验需要提升
接下来,我们先来看看如何解决第一个问题。
三、自动录制 case 方案
1、UI 自动化技术简介
UI 自动化的核心逻辑就是通过模拟用户的操作来实现自动化测试。模拟用户操作的关键技术就是如何查找页面元素。目前查找页面元素的方式大致有两种:图像识别和元素标识。图像识别是脱离页面 UI 组成结构,模拟人眼看图的逻辑来识别元素。比较流行的方案有图像比对、文字识别、AI技术识别等。这种方式的优点是不受程序实现方式与系统的限制,能够更加通用可靠的实现查找元素。缺点是对元素含义的识别比较困难,容易查找到错误的元素。元素识别是基于代码结构,通过界面上的元素标识、path 路径、文字等方式来定位一个页面元素。这种方式的优点是能够比较准确的找到业务需要的元素,缺点是受限于程序的实现方式和系统的限制,有时候获取元素标识会不稳定,给元素加标识工作量比较大。虽然工作量会大一点,但是通过元素标识的方式来查找页面元素目前对我们来说是更可行可靠的方案,所以我们是基于元素标识的方式来实现自动化 case 的录制回放。就元素标识识别的方式来说,它在实践的过程中也经历了一些发展演变:
| | | |
---|
| 最早基于绝对路径来查找元素,也就是完全根据元素树来生成元素的唯一定位标识 | | 维护性太差,代码稍微一改动造成路径变化,case就都不能用了 |
| 为了解决绝对路径元素标识经常变化的问题,通过给元素加标签,或者使用类似 xpath 的方式来进行相对路径的查找 | 代码改动对元素标识查找影响可控,case维护成本大大降低 | case 脚本编写成本高,需要人工加标签并编写 case 脚本 |
| 将元素标识与页面行为进行封装,case 编写人员只需要调用页面行为方法编写脚本即可 | 降低了 case 脚本编写成本,如果业务发生了变化,只需要改动一个地方就可以保证所有的 case 正常运行 | 对脚本编写人员的代码设计能力要求比较高,需要设计通用可靠复用性高的PageObject |
2、TARS 编写 case 方式
TARS 编写自动化测试 case 是基于POM原理。过程如下图:
首先,我们先手动在代码中给要操作的元素添加标签,比如 com.Qunar:id/ato_flight_tv_dep_city第二,为了避免代码迭代对 case 维护造成影响,我们在 case 脚本中将标签封装成了元素组件 arr_city,这样,即使界面结构发生了变化,只要该元素还存在,就仍然能准确找到这个元素,并不需要经常维护。第三,然后我们封装了基于元素的操作行为,比如选择到达城市。在实际编写 case 的时候,QA 同学就可以直接调用交互方法来组成一个 case 操作流程。第四,在执行 case 的时候,我们会通过数据工具拉取线上符合我们 case 要求的数据环境,然后在数据环境的指导下操作 APP 执行 case。第五,最终 case 脚本按照固定的步骤执行,如果元素标签发生了变化或者元素功能需要维护,只需要修改对应的标签或方法即可,并不需要对 case 脚本进行修改。这种方式已经非常好的解决了脚本编写工作量大,维护成本高的问题。但是在实际落地过程中,QA人员仍然会因为 case 编写难度高而很少新增新的 case。为了思考如何更进一步的降低 case 编写成本,我们将编写 case 核心功能点抽象如下:
抽象出来编写 case 的核心功能点之后,我们发现了一些问题:
- 手动维护 UI 元素标签:标签维护工作量大,每次有标签的修改还要重新发布版本,而且人工编写容易出错。
- case 编写学习门槛高:虽然我们已经很大程度上对页面的交互进行了封装,QA 同学只需要调用即可,但是总会遇到未封装的行为,对于 QA 同学的编码设计能力有一定的要求。
- 使用线上数据执行 case:线上数据经常会出现变化,需要不断的维护新增的预期外的流程。而且容易造成 case 执行的不稳定。
这些问题就造成了 QA 觉得编写 case 是一种负担,导致了 case 覆盖度不够,进一步导致召回率低。使用线上数据执行以及 case 编写质量参差不齐造成了准确率的降低。case 执行的准确率低就会造成 case 重试次数增多,导致执行时间长,然后就影响了拦截跳过率。所以我们需要思考能不能使用自动生成标签、自动生成脚本、使用稳定的数据来执行 case 的自动录制 case 的方式来编写 case。3、如何自动录制 case
能够自动添加标签、自动编写脚本,这种 case 录制方式我们叫它“自动录制 case”。理论上,如果程序代码一样、数据一样的话,一样的操作就能复现一样的结果。在这个基础上,如果用最新开发的代码进行回放,数据一样,操作一样,但是结果不一样,那就可能是两个原因,一个是 case 需要维护,一个是代码写错了。case 需要维护的情况一般来说是相关功能进行了改版,老 case 不适用于新业务。这种情况肯定是要维护 case 的。自动化测试的意义在于将重复的劳动自动化执行。也就是说,是对老功能进行回归,而不是对新功能进行测试,所以由于新功能的上线造成一些老 case 被废弃是必然的。那么把这部分 case 去掉,其他的没有执行通过的 case,大概率就是测出bug了,所以用录制回放的机制来作为自动化测试的 case 是可行的。想要把用户的操作行为记录下来进行回放,首先要解决如何记录元素的问题。也就是自动生成元素唯一标签,通过唯一标签来定位元素。这种方式看上去和基于绝对路径的 case 录制很像,因为之所以使用绝对路径生成 case,就是因为它是天然的元素唯一标识,但是这样我们的元素标识就会非常不稳定。稍微有一点变化就会导致找不到以前的标识。那么如何既能够自动生成元素标签,又可以一定程度上比较稳定呢?首先我们要思考的是标签生成的规范是什么?经过我们的思考,主要有两点:
1. 页面唯一。标签必须是唯一的,不然可能回放 case 的时候会点错元素,导致最后测试结果有误。
2. 一定程度上能够抵抗 DOM 树的变化,不会因为业务迭代而发生频繁的标签改变。
经过研究,我们认为用组件树的方式来生成标签比较符合要求。
首先,一个页面的元素在我们的代码中是有一个组件层级归属关系的。比如之前到达城市那个按钮,它的索引关系为机票首页-搜索面板-单程面板-到达城市,一般来说,我们的代码都是会按照页面、模块、组件这样的方式来进行组织。这就天然形成了一种标签的规则,就是按照组件层级进行唯一定位。
但是在实际操作中有一些特例。比如,某些元素是通过 for 循环来生成的,这样每一个元素标识都是一样的,于是我们就追加每一个元素的key来唯一标识。还有可能在一个代码文件中同一个元素写在了多个地方,这种情况我们就用元素所在的代码行数来唯一标识。
这样,我们生成的标签大概是这个样子的:
flight|OrderFillView|OrderFillPage_NewPassengerCard_ListPassengers_ToAddPassengerListView_ToAddPassengerItemView|TouchableOpacity_line=110可以看到,这个标签可以清晰的标识出元素的业务线、页面、业务模块、组件类型。标签规则定好了,我们就可以按照这个规则自动生成元素标签。为了避免人为破坏标签,我们选择在代码编译期通过插件的方式将标签信息注入到相关的元素组件上。这样,开发人员不会在代码中看到他们,避免了人为修改的维护问题。标签自动生成一定是基于相关代码版本的,一旦代码发生了变化,很可能重新生成的标签和上一次的标签不一样。这样我们基于上一次标签版本录制的 case 就跑不了了,因为元素都找不到了。
那么对于组件层级生成的标签,是否会因为代码的改变而经常需要维护?一般如果只是简单的增加新功能,一般是不会破坏组件层级的。而列表项的唯一标识是与数据相关,只要数据不变,key 就不会变。根据之前的分析,我们对于通过 line 标识来进行唯一性区分的标签可能会频繁的发生变化,导致 case 执行失败。所以对于这种情况,我们需要尽量通过自动维护标签的方式来避免增加人工维护的工作量。那么我们能否识别出新老版本的标签对应关系,将他们进行一致性关联?
我们先梳理一下代码变化对标签产生的影响可能有那些。第一种情况是标签所在的元素组件代码没有修改,但是代码文件其他地方产生了变化。对于第二种和第三种,我们肯定是要人工对 case 进行维护,因为这种情况属于业务本身产生了变化,以前的 case 不适用了。而第一种情况是我们需要进行自动关联的情况。我们可以通过 diff 的方式来识别新标签以前是哪一个标签,然后在跑 case 之前先进行标签的关联,然后再跑 case。
所谓记录就是要在可交互元素触发操作的时候,记录下来哪个元素在哪个时间被做了什么操作。所以,我们需要拦截元素的事件回调方法,先记录埋点,再调用以前的逻辑。我们通过装饰器模式,在编译期对可交互元素进行包装,这样,QA 操作元素的时候就会先记录埋点。埋点信息里面包含元素标签、业务线、所在页面、所在模块、操作时间、操作类型、代码版本等信息。然后在录制 case 的时候,我们将埋点数据保存下来,形成用户操作列表。case 即是将这个操作列表的操作翻译成自动化脚本进行模拟点击。现在我们自动将 QA 的操作记录了下来生成了自动化脚本,同时还需要将数据现场进行保存,不然我们就无法正确的回放 case。在 QA 操作 APP 的时候,我们会将每一个接口请求的请求数据和返回数据进行存储,当回放的时候,我们按顺序使用本地的接口数据替换每一个接口请求的返回值,也就是 mock 。这样,就可以保证 case 脚本所操作元素都在,除非代码进行了迭代。而且使用 mock 数据,避免了复杂的线上数据场景,增强了 case 执行的稳定性。这样,我们录制 case 就是将 QA 的操作和数据存储下来,执行 case 就是用这个数据在被测代码中进行回放。一个 case,要有一个结果来判定是否通过。一般来讲,我们是通过断言来判定 case 最后是否执行成功。我们来看一个例子:首页->商品列表->选择商品->商品详情->购买->联系人->进入确认订单页
当上面这个 case 执行完之后,我们需要判断,是否确定“进入了确认订单页”。这个操作我们当然也不能手写。为此,我们与算法组合作,引入了“智能 OCR 识别”功能。普通的 OCR 识别是匹配页面上的文字,但是对于我们的场景来说还不能完全满足,因为可能页面上有多个一样的文字。所以我们还需要知道文字所在的上下文,比如位置,相关的其他文案等。“智能 OCR 识别”功能可以先将页面模块进行划分,然后识别每一个模块中我们要查找的文字,然后返回文字所在模块的所有信息。这样,我们就可以精准定义我们的断言条件了。
有了“智能OCR识别”,我们可以将断言写成一个条件,作为 case 的一个属性信息一起保存在 case 数据中。不需要手动编写代码脚本来判断。
业务逻辑的断言验证完还不算 case 真正通过,我们还需要验证数据的断言。
因为我们使用的 mock 数据来跑 case,即是我们代码写错将请求参数拼错了,也不会影响数据返回结果。所以如果出现了这种 bug,我们就感知不到了。所以,我们需要对被测代码中的请求数据进行校验。
由于我们保存了接口的请求数据,理论上如果跑 case 的时候,生成的请求数据和保存的请求数据完全一致,那肯定是没问题的。但是由于业务迭代,请求数据一般会发生变化,所以我们要比对的是关键的数据节点。关键数据节点前后完全一致,就可以认为请求数据的逻辑是对的。
业务断言和数据断言都通过,一个 case 就算是通过了。
自动录制 case 的机制完成以后,流程变成了下图样子,基本上完全实现了自动化,降低了 case 编写的难度:录制完 case 之后,只需要在平台上进行上传即可。自动录制 case 解决了 case 编写难的问题,也一定程度上提高了自动化任务的准确率。下面我们来看看如何提升框架稳定性,降低接入成本,充分利用设备,优化操作体验。四、全新的自动化平台
第一个问题解决了,然后为了解决其他的问题,我们也对整个自动化系统进行了升级,包括基础设施、调度平台、报告等。1、TARS基础设施升级
系统中的问题都会造成 case 执行的准确率下降。以前,我们是每一个业务线自己维护一套设备。由于不同业务线执行自动化任务的频率不一样,就造成了设备使用上的浪费,有大量的手机一直在闲置。
为了解决这个问题,我们基于 STF 打造了分布式的设备调度系统。更全面的覆盖了手机设备型号,重复利用设备能力,由更灵活的设备调度策略来管理 case 运行。
为了进一步提升用户体验,满足开发或 QA 同学对自动化平台的各种需求,我们打造了全新的自动化平台,提供了丰富的功能,优化了日常使用的效率。
云测平台不仅仅能够支持日常的 case 任务执行,还能支持远程真机租用、性能等专项测试、行为回溯等功能,为大家提供更多基于自动化技术的服务。
五、总结规划
业务线接入非常方便,录制 case 也很方便
拦截跳过率降到了 8% 以内
准确率达到了 95% 以上
平均一个自动化任务执行时间在10分钟以内
但是在实际场景中,自动录制 case 并不能完全取代手动编写 case。因为它需要用 mock 数据进行测试,也就是说,对于需要使用线上数据进行测试的场景就无能为力了。 | | |
---|
| 端到端测试。比如一些后端的发布,需要前端的自动化测试对一些业务场景进行覆盖。 | |
| 前端发布测试。对于只关注前端 bug 的测试,自动录制 case 是可以胜任的。 | |
所以手动编写 case 本身还需要进一步的提升体验,比如未来我们会通过强大的 IDE 功能来简化 case 编写操作,也可能会借助 AI 来帮助处理一些广告弹窗等 case 未覆盖的问题。