找回密码
 会员注册
查看: 19|回复: 0

美团App页面视图可测性改造实践

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
69864
发表于 2024-10-9 03:03:48 | 显示全部楼层 |阅读模式
总第463篇2021年 第033篇一次编写多处运行的动态化容器技术给研发效率带来了极大的提升,但对于依旧需要多端验证的测试流程来说,在效率层面却面临着极大的挑战。本文围绕动态化容器中的动态布局技术,阐述了如何通过可测性改造来帮助达成提升测试效率的目标。希望可以给同样需要测试动态化页面的同学们带来一些启发和帮助。美团App的页面特点自动化测试实施中的技术挑战页面元素无法定位Appium元素定位的原理AccessibilityNodeInfo和Drawable页面视图可测性改造-XraySDK定位方案对比视图信息的获取和存储-XrayDumper视图信息的输出-XrayServerSDK整体功能结构视图信息的增强动态布局自动化的收益未来展望使用视图解析原理解决WebView元素定位视图可测性改造更多的应用场景美团App的页面特点对于不同的用户,美团App页面的呈现方式其实多种多样,这就是所谓的“千人千面”。以美团首页的“猜你喜欢”模块为例,针对与不同的用户有单列、Tab、双列等多种不同形式。这么多不同的页面样式需求,如果要在1天内时间内完成开发、测试、上线流程,研发团队也面临着很大的挑战。所以测试工程师就需要重度依赖自动化测试来形成快速的验收机制。图1 美团App首页多种页面布局样式自动化测试实施中的技术挑战接下来,本文将会从页面元素无法定位、Appium元素定位的原理、AccessibilityNodeInfo和Drawable等三个维度进行阐述。页面元素无法定位图2 页面元素审查情况目前,美团App客户端自动化主要依托于Appium(一个开源、跨平台的测试框架,可以用来测试原生及混合的移动端应用)来实现页面元素的定位和操作,当我们通过Appium Inspector进行页面元素审查时,能通过元素审查找到的信息只有外面的边框和下方的两个按钮,其他信息均无法识别(如上图2所示)。中央位置的图片、左上角的文本信息都无法通过现有的UI自动化方案进行定位和解析。不能定位元素,也就无法进行页面的操作和断言,这就严重影响了自动化的实施工作。经过进一步的调研,我们发现这些页面卡片中大量使用Drawable对象来绘制页面的信息,从而导致元素无法进行定位。为什么Drawable对象无法定位呢?下面我们一起研究一下UI自动化元素定位的原理。Appium元素定位的原理目前的UI自动化测试,使用Appium进行页面元素的定位和操作。如下图所示,AppiumServer和UiAutomator2的手机端进行通信后完成元素的操作。图3 Appium的通信原理通过阅读Appium源码发现完成一次定位的流程如下图所示:图4 Appium定位元素的实现流程首先,Appium通过调用findElement的方式进行元素定位。然后,调用Android提供UIDevice对象的findObject方法。最终,通过PartialMatch.accept完成元素的查找。接下来我们看一下,这个PartialMatch.accept到底是如何完成元素定位的。通过对于源码的研究,我们发现元素的信息都是存储在一个叫做AccessibilityNodeInfo的对象里面。源码中使用大量node.getXXX方法中的信息,大家是否眼熟呢?这些信息其实就是我们日常自动化测试中可以获取UI元素的属性。图5 AppiumInspector审查元素获取信息示意Drawable无法获取元素信息,是否和AccessibilityNodeInfo相关?我们进一步探究Drawable和AccessibilityNodeInfo的关系。AccessibilityNodeInfo和Drawable通过对于源码的研究,我们绘制了如下类图来解释AccessibilityNodeInfo和Drawable之间的关系。图6 类关系示意图View实现了AccessibilityEventSource接口并实现了一个叫做onInitializeAccessibilityNodeInfo的方法来填充信息。我们也在Android官方文档中找到了对于此信息的说明:onInitializeAccessibilityNodeInfo() :此方法为无障碍服务提供有关视图状态的信息。默认的View实现具有一组标准的视图属性,但如果您的自定义视图提供除了简单的 TextView或Button之外的其他互动控件,则您应替换此方法并将有关视图的其他信息设置到由此方法处理的AccessibilityNodeInfo对象中。而Drawable并没有实现对应的方法,所以也就无法被自动化测试找到。探究了元素查找原理之后,我们就要开始着手解决问题了。页面视图可测性改造-XraySDK定位方案对比既然知道了Drawable没有填充AccessibilityNodeInfo,也就说明我无法接入目前的自动化测试方案来完成页面内容的获取。那我们可以想到如下三种方案来解决问题:实现方案影响范围改造Appium定位方式,让Drawable可以被识别需要改动底层的AccessibilityNodeInfo obtain(View,int)方法和为Drawable添加AccessibilityNodeInfo这样就需要对于所有的Android系统做兼容,影响范围过大使用View替代Drawable动态布局卡片使用Drawable进行绘制就是因为Drawable比View使用资源更少,绘制性能更好,放弃使用Drawable就等于放弃了性能的改进使用图像识别进行定位动态卡片中有很多图像中包含文字,还有多行文本都会对图像识别的准确性带来很大的影响上面的三种方案,目前看来都无法有效地解决动态卡片元素定位的问题。如何在影响范围较小的前提下,达成获取视图信息的目标呢?接下来,我们将进一步研究动态布局的实现方案。视图信息的获取和存储-XrayDumper我们的应用场景非常明确,自动化测试通过集成Client来获得和客户端交互能力,通过Client向App发送指令来页面信息的获取。那我们可以考虑内嵌一个SDK(XraySDK)来完成视图的获取,然后再向自动化提供一个客户端(XrayClient)来完成这部分功能。图7 XraySDK的工作流程示意图对于XraySDK的功能划分,如下表所示:模块名功能划分运行环境产品形态Xray-Client1.和Xray-Server进行交互进行指令发送和数据的接收2.暴露对外的Api给自动化或者其他系统App内部客户端SDK(AAR和Pod-Library)Xray-SDK1.进行页面信息的获取以及结构化(Xray-Dumper)2.接收用户指令来进行结构化数据输出(Xray-Server)自动化内部或者三方系统内部JAR包或基于其他语言的依赖包XraySDK如何才能获取到我们需要的Drawable信息呢?我们先来研究一下动态布局的实现方案。图8 动态卡片的页面绘制流程动态布局的视图呈现过程分为:解析模板->绑定数据->计算布局->页面绘制,计算布局结束后,元素在页面上的位置就已经确定了,那么只要拦截这个阶段信息就可以实现视图信息的获取。通过对于代码的研究,我们发现在com.sankuai.litho.recycler.AdapterCompat这个类中控制着视图布局行为,在bindViewHolder中完成视图的最终的布局和计算。首先,我们通过在此处插入一个自定义的监听器来拦截布局信息。public?final?void?bindViewHolder(BaseViewHolder?viewHolder,?int?position)?{????????if?(viewHolder?!=?null)?{????????????viewHolder.bindView(context,?getData(position),?position);????????????//自动化测试回调????????????if?(componentTreeCreateListeners?!=?null)?{????????????????if?(viewHolder?instanceof?LithoViewHolder)?{????????????????????DataHolder?holder?=?getData(position);????????????????????//获取视图布局信息????????????????????LithoView?view?=?((LithoViewHolder)?viewHolder).lithoView;????????????????????LayoutController?layoutController?=?((LithoDynamicDataHolder)?holder).getLayoutController(null);????????????????????VirtualNodeBase?node?=?layoutController.viewNodeRoot;????????????????????//通过监听器将视图信息向外传递给可测性SDK????????????????????componentTreeCreateListeners.onComponentTreeCreated(node,?view.getRootView(),?view.getComponentTree());????????????????}????????????}????????}????}然后,通过暴露一个静态方法给可测性SDK,完成监听器的初始化。public?static?void?setComponentTreeCreateListener(ComponentTreeCreateListener?l)?{????????AdapterCompat.componentTreeCreateListeners?=?l;????????try?{????????????//?兼容mbc的动态布局自动化测试,为避免循环依赖,采用反射调用????????????Class?mbcDynamicClass?=?Class.forName("com.sankuai.meituan.mbc.business.item.dynamic.DynamicLithoItem");????????????Method?setComponentTreeCreateListener?=?mbcDynamicClass.getMethod("setComponentTreeCreateListener",?ComponentTreeCreateListener.class);????????????setComponentTreeCreateListener.invoke(null,?l);????????}?catch?(Exception?e)?{????????????e.printStackTrace();????????}????????try?{????????????//?搜索新框架动态布局自动化测试????????????Class?searchDynamicClass?=?Class.forName("com.sankuai.meituan.search.result2.model.DynamicItem");????????????Method?setSearchComponentTreeCreateListener?=?searchDynamicClass.getMethod("setComponentTreeCreateListener",?ComponentTreeCreateListener.class);????????????setSearchComponentTreeCreateListener.invoke(null,?l);????????}?catch?(Exception?e)?{????????????e.printStackTrace();????????}????}最后,自动化通过设置自定义的监听器来完成视图信息的获取和存储。//通过静态方法设置一个ComponentTreeCreateListener来监听布局事件AdapterCompat.setComponentTreeCreateListener(new?AdapterCompat.ComponentTreeCreateListener()?{????????????@Override????????????public?void?onComponentTreeCreated(VirtualNodeBase?node,?View?rootView,?ComponentTree?tree)?{????????????????//将信息存储到一个自定义的ViewInfoObserver对象中????????????????ViewInfoObserver?vif?=?new?ViewInfoObserver();????????????????vif.update(node,?rootView,?tree);????????????}????????});我们将视图信息存储在ViewInfoObserver这样一个对象中。public?class?ViewInfoObserver?implements?AutoTestObserver{????public?static?HashMap?VIEW_MAP?=?new?HashMap();????public?static?HashMap?VIEW?=?new?HashMap();????public?static?HashMap?COMPTREE_MAP?=?new?HashMap();????public?static?String?uri?=?"http://dashboard.ep.dev.sankuai.com/outter/dynamicTemplateKeyFromJson";????@Override????public?void?update(VirtualNodeBase?vn,?View?view,ComponentTree?tree)?{????????if?(null?!=?vn?&?null?!=?vn.jsonObject)?{????????????try?{????????????????String?string?=?vn.jsonObject.toString();????????????????Gson?g?=?new?GsonBuilder().setPrettyPrinting().create();????????????????JsonParser?p?=?new?JsonParser();????????????????JsonElement?e?=?p.parse(string);????????????????String?templateName?=?null;????????????????String?name1?=?getObject(e,"templateName");????????????????String?name2?=?getObject(e,"template_name");????????????????String?name3?=?getObject(e,"template");????????????????templateName?=?null?!=?name1???name1?:?(null?!=?name2???name2?:?(null?!=?name3???name3?:?null));????????????????if?(null?!=?templateName)?{????????????????//如果已经存储则更新视图信息????????????????????if?(VIEW_MAP.containsKey(templateName))?{????????????????????????VIEW_MAP.remove(templateName);????????????????????}????????????????????//存储视图编号????????????????????VIEW_MAP.put(templateName,?view);????????????????????if?(VIEW.containsKey(templateName))?{????????????????????????VIEW.remove(templateName);????????????????????}????????????????????//存储视图信息????????????????????VIEW.put(vn,?view);????????????????????if?(COMPTREE_MAP.containsKey(templateName))?{????????????????????????COMPTREE_MAP.remove(templateName);????????????????????}????????????????????COMPTREE_MAP.put(templateName,?tree);????????????????????System.out.println("autotestDyn:update success");????????????????}?????????????}?catch?(Exception?e)?{????????????????System.out.println(e.toString());????????????????System.out.println("autotestDyn:templateName not exist!");????????????}????????}????}当需要查询这些信息的时候,就可以通过XrayDumper来完成信息的输出。public?class?SubViewInfo?{????public?JSONObject?getOutData(String?template)?throws?JSONException?{????????JSONObject?outData?=?new?JSONObject();????????JSONObject?componentTouchables?=?new?JSONObject();????????if?(!COMPTREE_MAP.isEmpty()?&?COMPTREE_MAP.containsKey(template)?&?null?!=?COMPTREE_MAP.get(template))?{????????????ComponentTree?cpt?=?COMPTREE_MAP.get(template);????????????JSONArray?componentArray?=?new?JSONArray();????????????ArrayList?touchables?=?cpt.getLithoView().getTouchables();????????????LithoView?lithoView?=?cpt.getLithoView();????????????int[]?ls?=?new?int[2];????????????lithoView.getLocationOnScreen(ls);????????????int?pointX?=?ls[0];????????????int?pointY?=?ls[1];????????????for?(int?i?=?0;?i??0)?{????????????????for?(int?i?=?0;?i??gestures?=?new?ArrayList();if?(view.isClickable()){???gestures.add("isClickable");}if?(view.isLongClickable()){???gestures.add("isLongClickable");}//省略部分代码.....动态布局自动化的收益基于视图可测性的提升,美团动态化卡片的自动化测试覆盖度有了大幅的提升,从原来无法做自动化测试,到目前80%以上的动态化卡片都实现了自动化测试,而且效率也得到了明显的提升。图10 自动化效率提升收益未来展望页面视图信息作为客户端测试最基础且重要的属性之一,是对用户视觉信息的一种代码级的表示。它对于机器识别页面元素信息有着非常重要的作用,对于它的可测性改造将会给技术团队带来很大的收益。我们会列举了几个视图可测性改造的探索方向,仅供大家参考。使用视图解析原理解决WebView元素定位应用同样的思想,我们还可以用来解决WebView元素定位的问题。图11 WebView页面示例通过运行在App内部的SDK,可以获取到对应的WebView实例。通过获取到根节点,从根节点开始进行循环遍历,同时把每个节点的信息存储下来就可以得到所有的视图信息了。在WebView是否也有同样合适的根节点呢?基于对于HTML的理解,我们可以想到HTML中所有的标签都是挂在BODY标签下面的,BODY标签就是我们需要选取的根节点。我们可以通过WebElement["attrName"]的方式来进行属性的获取。图12 遍历WebView节点的代码示例视图可测性改造更多的应用场景提升功能测试可靠性:在功能测试自动化中,通过内部更加稳定和迅速的视图信息输出,可以有效提升自动化测试的稳定性。避免由于元素无法获取或者元素获取缓慢导致的自动化测试失败。提升可靠性测试效率:对于依靠随机或者按照视图信息进行页面随机操作的可靠性测试,依赖对于视图信息的过滤,也可以只操作可以交互的元素(通过过滤元素事件监听器是否为空)。这样就可以有效提升可靠性测试的效率,在单位时间内可以完成更多页面的检测。增加兼容性测试检测手段:在页面兼容性方面,通过对页面组件位置信息和属性来扫描页面内是否存在不合理的堆叠、空白区域、形状异常等UI呈现异常。也可以获取内容信息,例如图片、文本,来检查是否存在不适宜内容呈现。可以作为图像对比方案的有效补充。阅读更多---前端??算法?后端?数据安全?Android?iOS??运维?测试----------? END? ----------招聘信息美团平台质量技术中心,负责美团 App 业务和大前端(移动客户端和Web前端)基础技术质量工作,沉淀流程规范和配套工具、提升研发效率。团队技术一流、氛围良好,感兴趣的同学简历可以发送至: zhangjie63@meituan.com也许你还想看? 美团智能支付稳定性测试实战? 质量运营在美团智能支付业务测试中的初步实践? Lego:美团接口自动化测试实践
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-7 07:31 , Processed in 0.515573 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表