惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
C
CERT Recently Published Vulnerability Notes
P
Proofpoint News Feed
Y
Y Combinator Blog
T
The Blog of Author Tim Ferriss
云风的 BLOG
云风的 BLOG
H
Help Net Security
Recorded Future
Recorded Future
The Register - Security
The Register - Security
F
Full Disclosure
N
Netflix TechBlog - Medium
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hackread – Cybersecurity News, Data Breaches, AI and More
爱范儿
爱范儿
Security Archives - TechRepublic
Security Archives - TechRepublic
Simon Willison's Weblog
Simon Willison's Weblog
Cisco Talos Blog
Cisco Talos Blog
I
InfoQ
T
Tenable Blog
T
Tor Project blog
人人都是产品经理
人人都是产品经理
D
DataBreaches.Net
NISL@THU
NISL@THU
Google DeepMind News
Google DeepMind News
博客园 - 叶小钗
B
Blog
V
V2EX
Jina AI
Jina AI
L
LangChain Blog
月光博客
月光博客
W
WeLiveSecurity
U
Unit 42
AWS News Blog
AWS News Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园 - 聂微东
V
Visual Studio Blog
A
Arctic Wolf
T
Tailwind CSS Blog
The Cloudflare Blog
SecWiki News
SecWiki News
S
SegmentFault 最新的问题
Hacker News - Newest:
Hacker News - Newest: "LLM"
宝玉的分享
宝玉的分享
MyScale Blog
MyScale Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Securelist
www.infosecurity-magazine.com
www.infosecurity-magazine.com
腾讯CDC
雷峰网
雷峰网

美团技术团队

美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团发布基于 N-gram 全新模型:嵌入扩展新范式,实现轻量化 MoE 高效进化 2025美团技术年货,「马」上到来 多维创新打造强泛化智能体模型,LongCat-Flash-Thinking-2601技术报告发布 美团 EvoCUA 刷新开源 SOTA,会用电脑还会持续进化的智能体! 美团 LongCat-Flash-Thinking-2601 发布,工具调用能力登顶开源 SOTA! KuiTest:基于大模型通识的 UI 交互遍历测试 AAAI 2026 | 美团技术团队学术论文精选 2025 | 美团技术团队热门技术文章汇总 美团 LongCat-Video-Avatar 正式发布,实现开源 SOTA 级拟真表现 大模型剪枝新范式:先浓缩,再剪枝——DenoiseRotator技术解读 LongCat 上线 AI 生图!精准高效,AI 创作不设限 美团发布 LongCat-Image 图像生成模型,编辑能力登顶开源 SOTA AI Coding与单元测试的协同进化:从验证到驱动 R-HORIZON:探索长程推理边界,复旦NLP&美团LongCat联合提出LRMs能力评测新框架 美团 LongCat 发布 AMO-Bench:突破 AIME 评测饱和困境,重新定义 LLM 数学上限 美团 LongCat Interaction 团队发布大模型交互系统技术报告 WOWService 美团 LongCat 团队发布全模态一站式评测基准 UNO-Bench 美团开源LongCat-Audio-Codec,高效语音编解码器助力实时交互落地 NeurIPS 2025 | 美团技术团队论文精选 LongCat-Flash-Omni正式发布并开源:开启全模态实时交互时代 美团 LongCat 团队发布 VitaBench:基于复杂生活场景的交互式 Agent 评测基准 LongCat-Video 视频生成模型正式发布,探索世界模型的第一步 ICCV 2025 | 美团论文精选及多模态推理竞赛冠军方法分享 从0到1建设美团数据库容量评估系统 可验证过程奖励在提升大模型推理效率中的探索与实践 LongCat-Flash-Thinking 正式发布,更强、更专业,保持极速! 开源 | InfiniteTalk:无限长虚拟人视频生成的新范式 美团正式发布并开源 LongCat-Flash-Chat,动态计算开启高效 AI 时代 美团 M17 团队开源 Meeseeks 评测集:揭秘大模型的“听话”能力 可信实验白皮书系列08:开放式分析引擎 | 附PDF合集 美团智能头盔研发实践系列02:软件功能篇 美团智能头盔研发实践系列01:硬件设计篇 ACL 2025 | 美团技术团队论文精选 美团开源OIBench与CoreCodeBench:揭示大模型编程能力的真实水平 可信实验白皮书系列07:高阶实验工具 开源 | MeiGen-MultiTalk:基于单张照片实现多人互动演绎 可信实验白皮书系列06:观察性研究 JDK高版本特性总结与ZGC实践 可信实验白皮书系列05:准实验 可信实验白皮书系列04:随机轮转实验 可信实验白皮书系列03:随机对照实验 可信实验白皮书系列02:AB实验基础 可信实验白皮书系列01:从0到1的方法论与实践指南 MTGR:美团外卖生成式推荐Scaling Law落地实践 OR算法+ML模型混合推理框架架构演进 ICLR&CVPR 2025美团技术团队论文精选 行为正则化与顺序策略优化结合的离线多智能体学习算法 预测技术在美团弹性伸缩场景的探索与应用 美团技术年货 | 600+页电子书,算法、工程、测试、数据、安全系列大合集 鸿蒙应用签名实操及机制探究 2024 | 美团技术团队热门技术文章汇总 AutoConsis:UI内容一致性智能检测 CIKM 2024 | 美团技术团队精选论文解读 大前端:如何突破动态化容器的天花板? KDD 2024 OAG-Challenge Cup赛道三项冠军技术方案解读 新一代实验分析引擎:驱动履约平台的数据决策 大众点评技术部包揽KDD 2024 OAG-Challenge Cup赛道全部3项冠军 ACL 2024 | 美团技术团队精选论文解读 KDD 2024 | 美团技术团队精选论文解读 基本功 | 一文讲清多线程和多线程同步 小程序可测性能力建设与实践 SIGIR 2024 | 美团技术团队精选论文解读 Spark向量化计算在美团生产环境的实践 CVPR 2024 | 美团技术团队精选论文解读 基于多模态信息抽取的菜品知识图谱构建 DDD在大众点评交易系统演进中的应用 美团外卖基于GPU的向量检索系统实践 美团大规模KV存储挑战与架构实践 基于接口数据变异的App健壮性测试实践 美团技术年货 | 600+页电子书,前端、后端、算法、测试、运维系列大合集 美团RASP大规模研发部署实践总结 2023 | 美团技术团队热门技术文章汇总 美团到店终端从标准化到数字化的演进之路 AIOps在美团的探索与实践——事件管理篇 美团技术博客十周年,感谢一路相伴 基于UI交互意图理解的异常检测方法 如何利用「深度上下文兴趣网络」提升点击率? 基于模式挖掘的可靠性治理探索 代码变更风险可视化系统建设与实践 美团多场景建模的探索与实践 MJDK 如何实现压缩速率的 5 倍提升? 如何提供一个可信的AB测试解决方案 KDD 2023 | 美团技术团队精选论文解读 美团前端研发框架Rome实践和演进趋势 斩获CVPR 2023竞赛2项冠军|美团街景理解中视觉分割技术的探索与应用 MySQL自治平台建设的内核原理及实践(下) CVPR 2023 | 美团技术团队精选论文解读 超大规模数据库集群保稳系列之三:美团数据库容灾体系建设实践 超大规模数据库集群保稳系列之二:数据库攻防演练建设实践 Robust 2.0:支持Android R8的升级版热修复框架 超大规模数据库集群保稳系列之一:高可用系统 一次「找回」TraceId的问题分析与过程思考 基于AI+数据驱动的慢查询索引推荐 SOTA!目标检测开源框架YOLOv6 3.0版本来啦
美团App页面视图可测性改造实践
张杰 · 2021-07-28 · via 美团技术团队

美团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-Client1.和Xray-Server进行交互进行指令发送和数据的接收
2.暴露对外的Api给自动化或者其他系统
自动化内部或者三方系统内部JAR包或基于其他语言的依赖包
Xray-SDK1.进行页面信息的获取以及结构化(Xray-Dumper)
2.接收用户指令来进行结构化数据输出(Xray-Server)
App内部客户端SDK(AAR和Pod-Library)

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;
                    //通过监听器将视图信息向外传递给可测性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<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";

    @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<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  {
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
    ....
        return super.onStartCommand(intent, flags, startId);
    }
}

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

public class AutoTestServer extends Service  {
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public 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  {
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
    //注册监听器
        AdapterCompat.setComponentTreeCreateListener(new AdapterCompat.ComponentTreeCreateListener() {
            @Override
            public 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