一次编写多处运行的动态化容器技术给研发效率带来了极大的提升,但对于依旧需要多端验证的测试流程来说,在效率层面却面临着极大的挑战。本文围绕动态化容器中的动态布局技术,阐述了如何通过可测性改造来帮助达成提升测试效率的目标。希望可以给同样需要测试动态化页面的同学们带来一些启发和帮助。

  • 美团App的页面特点

  • 自动化测试实施中的技术挑战

    • 页面元素无法定位

    • Appium元素定位的原理

    • AccessibilityNodeInfo和Drawable

  • 页面视图可测性改造-XraySDK

    • 定位方案对比

    • 视图信息的获取和存储-XrayDumper

    • 视图信息的输出-XrayServer

    • SDK整体功能结构

    • 视图信息的增强

    • 动态布局自动化的收益

  • 未来展望

    • 使用视图解析原理解决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相关?我们进一步探究DrawableAccessibilityNodeInfo的关系。

AccessibilityNodeInfo和Drawable

通过对于源码的研究,我们绘制了如下类图来解释AccessibilityNodeInfoDrawable之间的关系。

图6 类关系示意图

View实现了AccessibilityEventSource接口并实现了一个叫做onInitializeAccessibilityNodeInfo的方法来填充信息。我们也在Android官方文档中找到了对于此信息的说明:

onInitializeAccessibilityNodeInfo() :此方法为无障碍服务提供有关视图状态的信息。默认的View实现具有一组标准的视图属性,但如果您的自定义视图提供除了简单的 TextViewButton之外的其他互动控件,则您应替换此方法并将有关视图的其他信息设置到由此方法处理的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-Client 1.和Xray-Server进行交互进行指令发送和数据的接收
2.暴露对外的Api给自动化或者其他系统
App内部 客户端SDK(AAR和Pod-Library)
Xray-SDK 1.进行页面信息的获取以及结构化(Xray-Dumper)
2.接收用户指令来进行结构化数据输出(Xray-Server)
自动化内部或者三方系统内部 JAR包或基于其他语言的依赖包

XraySDK如何才能获取到我们需要的Drawable信息呢?我们先来研究一下动态布局的实现方案。

图8 动态卡片的页面绘制流程

动态布局的视图呈现过程分为:解析模板->绑定数据->计算布局->页面绘制,计算布局结束后,元素在页面上的位置就已经确定了,那么只要拦截这个阶段信息就可以实现视图信息的获取。

通过对于代码的研究,我们发现在com.sankuai.litho.recycler.AdapterCompat这个类中控制着视图布局行为,在bindViewHolder中完成视图的最终的布局和计算。首先,我们通过在此处插入一个自定义的监听器来拦截布局信息。

public final void bindViewHolder(BaseViewHolder<Data> 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<Data>) viewHolder).lithoView;LayoutController layoutController = ((LithoDynamicDataHolder) holder).getLayoutController(null);VirtualNodeBase node = layoutController.viewNodeRoot;//通过监听器将视图信息向外传递给可测性SDKcomponentTreeCreateListeners.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() {@Overridepublic 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<String, View> VIEW_MAP = new HashMap<>();public static HashMap<VirtualNodeBase, View> VIEW = new HashMap<>();public static HashMap<String, ComponentTree> COMPTREE_MAP = new HashMap<>();public static String uri = "http://dashboard.ep.dev.sankuai.com/outter/dynamicTemplateKeyFromJson";@Overridepublic 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<View> 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 < touchables.size(); i++) {JSONObject temp = new JSONObject();int height = touchables.get(i).getHeight();int width = touchables.get(i).getWidth();int[] tl = new int[2];touchables.get(i).getLocationOnScreen(tl);temp.put("height",height);temp.put("width",width);temp.put("pointX",tl[0]);temp.put("pointY",tl[1]);String url = "";try {EventHandler eh = (EventHandler) getValue(getValue(touchables.get(i), "mOnClickListener"), "mEventHandler");DynamicClickListener listener = (DynamicClickListener) getValue(getValue(eh, "mHasEventDispatcher"), "listener");Uri clickUri = (Uri) getValue(listener, "uri");if (null != clickUri) {url = clickUri.toString();}} catch (Exception e) {Log.d("autotest", "get click url error!");}temp.put("url",url);componentArray.put(temp);}componentTouchables.put("componentTouchables",componentArray);componentTouchables.put("componentTouchablesCount", cpt.getLithoView().getTouchables().size());View[] root = (View[])getValue(cpt.getLithoView(),"mChildren");JSONArray allComponentArray = new JSONArray();if (root.length > 0) {for (int i = 0; i < root.length; i++) {try {if (null != root[i]) {Object items[] = (Object[]) getValue(getValue(root[i], "mMountItems"), "mValues");componentTouchables.put("componentCount", items.length);for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {getMountItems(allComponentArray, items[itemIndex], pointX, pointY);}}} catch (Exception e) {}}}componentTouchables.put("componentUntouchables",allComponentArray);} else {Log.d("autotest","COMPTREE_MAP is null!");}outData.put(template,componentTouchables);System.out.println(outData);return outData;}}
}

视图信息的输出-XrayServer

我们获取到了信息,接下来就要考虑如何将视图信息传递给自动化测试脚本,我们参考了Appium的设计。

Appium通过在手机上安装的InstrumentsClient启动了一个SocketServer通过HTTP协议来完成自动化和底层测试框架的数据通信。我们也可以借鉴上述思路,在美团App中启动一个WebServer来完成信息的输出。

第一步,我们实现了一个继承了Service组件,这样就可以方便的通过命令行的方式的启动和停止可测性的功能。

public class AutoTestServer extends Service  {@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {....return super.onStartCommand(intent, flags, startId);}
}

第二步,通过HttpServer的方式对外暴露通信的接口。

public class AutoTestServer extends Service  {@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {// 创建对象,端口通过参数传入if (intent != null) {int randNum = intent.getIntExtra("autoTestPort",8999);HttpServer myServer = new HttpServer(randNum);try {// 开启HTTP服务myServer.start();System.out.println("AutoTestPort:" + randNum);} catch (IOException e) {System.err.println("AutoTestPort:" + e.getMessage());myServer = new HttpServer(8999);try {myServer.start();System.out.println("AutoTestPort:8999");} catch (IOException e1) {System.err.println("Default:" + e.getMessage());}}}return super.onStartCommand(intent, flags, startId);}
}

第三步,将之前设置好的监听器进行注册。

public class AutoTestServer extends Service  {@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {//注册监听器AdapterCompat.setComponentTreeCreateListener(new AdapterCompat.ComponentTreeCreateListener() {@Overridepublic void onComponentTreeCreated(VirtualNodeBase node, View rootView, ComponentTree tree) {ViewInfoObserver vif = new ViewInfoObserver();vif.update(node, rootView, tree);}});// 创建对象,端口通过参数传入.....return super.onStartCommand(intent, flags, startId);}
}

最后,在HttpServer中通过不同的路径来实现接收不同的指令。

private JSONObject getResponseByUri(@Nonnull IHTTPSession session) throws JSONException {String uri = session.getUri();if (isFindCommand(uri)) {return getResponseByFindUri(uri);}
}@Nonnull
private JSONObject getResponseByFindUri(@Nonnull String uri) throws JSONException {String template = uri.split("/")[2];String protocol = uri.split("/")[3];switch (protocol) {case "frame":TemplateLayoutFrame tlf = new TemplateLayoutFrame();return tlf.getOutData(template);case "subview":SubViewInfo svi = new SubViewInfo();return svi.getOutData(template);//省略了部分的代码处理逻辑    ....default:JSONObject errorJson = new JSONObject();errorJson.put("success", false);errorJson.put("message", "输入find链接地址有误");return errorJson;}
}

SDK整体功能结构

自动化脚本通过访问设备的特定端口(例如:http://localhost:8899/find/subview),经由XrayServer,通过访问路径将请求转发至XrayDumper进行信息的提取和输出。然后布局解析器将布局信息序列化成JSON数据,再经由XrayServer,通过网络以HTTP响应的方式传到给自动化测试脚本。

图9 XraySDK功能结构示意图

视图信息的增强

除了常规的位置、内容、类型等信息,我们还通过检查时间监听器的方式,进一步判断视图元素是否可以进行交互,进一步增强了页面视图结构的有效信息。

// setGestures
ArrayList<String> 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呈现异常。也可以获取内容信息,例如图片、文本,来检查是否存在不适宜内容呈现。可以作为图像对比方案的有效补充。

招聘信息

美团平台质量技术中心,负责美团 App 业务和大前端(移动客户端和Web前端)基础技术质量工作,沉淀流程规范和配套工具、提升研发效率。团队技术一流、氛围良好,感兴趣的同学简历可以发送至: zhangjie63@meituan.com

end


App自动化测试实施中的技术挑战相关推荐

  1. 零知识证明应用到区块链中的技术挑战

    零知识证明应用到区块链中的技术挑战 李康1,2, 孙毅1,2, 张珺3, 李军4, 周继华5, 李忠诚1 1. 中国科学院计算技术研究所,北京 100190 2. 中国科学院大学,北京 100049 ...

  2. Mobile App自动化测试技术及实现

    Mobile App自动化测试技术及实现 Android 自动化框架 iOS自动化测试框架 Appium测试框架的组成 Appium的工作流程 Appium Server Appium Inspect ...

  3. 2022年最新csdn涨薪技术栈-app自动化测试概述

    目录 一. 应用背景 二. 测试框架介绍 1.Android自动化框架 IOS自动化框架 三. 测试流程与分类 1.测试流程 2.自动化测试分类 四. 移动操作系统与app类型 1.移动操作系统and ...

  4. 聊聊语音聊天室app源码实时音视频中的技术难点:回声消除+噪声消除

    聊聊语音聊天室app源码实时音视频中的技术难点:回声消除+噪声消除 在聊聊语音聊天室app源码各个实时音视频互动场景中,回声和噪声对于影响用户体验而言都是很大的问题.音视频正在发展成为互联网线上沟通的 ...

  5. 国内机器视觉行业中工业相机的发展现状及其面临的技术挑战

    工业相机是机器视觉系统中的重要组成部分,其主要功能是将光信号转换成有序的电信号.因其抗干扰能力强.分辨率高.性能稳定可靠.传输速度快等优势,工业相机可被应用于恶劣的工作环境中,并且可以长时间连续工作. ...

  6. APP自动化测试中的Unlock和AppiumSetting反复安装问题

    本文带领大家了解一些APP自动化测试的问题. 1.前提 基于win10专业版64位系统+jdk1.8+python3+pycharm+android SDK+appium+unittest. 2.痛点 ...

  7. APP自动化测试-10.Appium中Desired Capabilities常用参数

    APP自动化测试-10.Appium中Desired Capabilities常用参数 文章目录 APP自动化测试-10.Appium中Desired Capabilities常用参数 前言 一.通用 ...

  8. PPT 下载 | 数据治理中的一些挑战与应用

    本文根据神策数据联合创始人 & CTO 曹犟在神策 2019 数据驱动大会的精英训练营上发表的<数据治理中的一些挑战与应用>主题演讲整理而成.本文将为你重点介绍: ·      数 ...

  9. 全球买全球卖 国际化的技术挑战

    改变世界的不是技术,而是技术背后的梦想.2015财年阿里巴巴中国零售平台实现了3万亿人民币交易额首次超越沃尔玛成为全球最大经济体,而这只是一个新的起点,我们的下一个目标是到2020年全球零售平台交易额 ...

最新文章

  1. html input type=file 的属性及api
  2. 大一期末考试,python,测试题,含答案
  3. html5 内嵌网页_HTML5与CSS3基础语法自学教程(二)
  4. 随手记录自动化常用的一些事情
  5. linux桌面效率提高,ElementaryOS:使用这个轻量级 Linux 桌面提高工作效率
  6. jquery-只对新用户弹一次窗
  7. 我写了一个java实体类,implements了Serializable接口,然后我如何让serialversionUID自动生成...
  8. 支持!解决卡巴斯基程序错误及程序断开的问题!
  9. open and openat
  10. 20210223-广东省通信管理局
  11. SolidWorks的发展历史(1994~2007)
  12. 中国拖车洒水器市场趋势报告、技术动态创新及市场预测
  13. Java数据类型和方法练习题
  14. 《一切都是最好的安排》之感想
  15. Oracle——03索引
  16. 三维点云处理06-2D/3DIoU计算
  17. latex中文支持问题,自动化学报latex模板问题
  18. 新版Qq为什么不受欢迎?
  19. 如何updateR的version
  20. Microsoft SQL Server 数据库体系结构图解

热门文章

  1. DAPI(Distributed Application Program Interface)
  2. JAVA模拟淘宝、天猫登录
  3. Openlayers之加载Stamen地图
  4. 研究方向三选一选择FPGA/计算机视觉/故障检测
  5. Python零基础入门学习笔记(一)
  6. 论文精读 ——《BEVDepth: Acquisition of Reliable Depth for Multi-view 3D Object Detection》
  7. 高德API支持WMS服务器,GCJ02-Correct
  8. java绘图技术,演示绘制不同的图形
  9. 台式计算机戴尔3020,戴尔 Dell OptiPlex 3020M 台式机整机 评测
  10. BZOJ 2140: 稳定婚姻 Tarjan Map