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

推荐订阅源

D
Docker
Simon Willison's Weblog
Simon Willison's Weblog
H
Help Net Security
F
Fortinet All Blogs
H
Heimdal Security Blog
S
Schneier on Security
L
LangChain Blog
博客园 - Franky
酷 壳 – CoolShell
酷 壳 – CoolShell
NISL@THU
NISL@THU
P
Palo Alto Networks Blog
J
Java Code Geeks
博客园 - 【当耐特】
The Last Watchdog
The Last Watchdog
W
WeLiveSecurity
www.infosecurity-magazine.com
www.infosecurity-magazine.com
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Vulnerabilities – Threatpost
I
InfoQ
Recorded Future
Recorded Future
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
CERT Recently Published Vulnerability Notes
T
Tenable Blog
腾讯CDC
C
Check Point Blog
量子位
M
MIT News - Artificial intelligence
GbyAI
GbyAI
罗磊的独立博客
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
B
Blog
小众软件
小众软件
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
CXSECURITY Database RSS Feed - CXSecurity.com
Stack Overflow Blog
Stack Overflow Blog
P
Proofpoint News Feed
P
Privacy & Cybersecurity Law Blog
V2EX - 技术
V2EX - 技术
T
Threatpost
Engineering at Meta
Engineering at Meta
Attack and Defense Labs
Attack and Defense Labs
T
Tailwind CSS Blog
S
Securelist
The Cloudflare Blog
博客园 - 叶小钗
L
LINUX DO - 最新话题
T
Troy Hunt's Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
爱范儿
爱范儿

美团技术团队

美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团 · 技术团队 美团发布基于 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版本来啦
Android增量代码测试覆盖率工具
武智 莹莹 周佳 · 2017-06-16 · via 美团技术团队

美团业务快速发展,新项目新业务不断出现,在项目开发和测试人员不足、开发同学粗心的情况下,难免会出现少测漏测的情况,如何保证新增代码有足够的测试覆盖率是我们需要思考的问题。

先看一个bug:

以上代码可能在onDestory时反注册一个没有注册的receiver而发生崩溃。如果开发同学经验不足、自测不够充分或者代码审查不够仔细,这个bug很容易被带到线上。

正常情况下,可以通过写单测来保证新增代码的覆盖率,在Android中可以参考《Android单元测试研究与实践》 。但在实际开发中,由于单测部署成本高、项目排期比较紧张、需求变化频繁、团队成员能力不足等多种原因,单测在互联网行业普及程度并不理想。

所以我们实现了这样一个工具,不需要写单测的情况下,在代码提交之前自动检测新增代码的手工测试覆盖率,避免新开发的功能没有经过自测就直接进入代码审查环节。

整个工具主要包含下面三个方面的内容:

  • 如何获取新增代码。
  • 如何只生成新增代码的覆盖率报告。
  • 如何让整个流程自动化。

定义新增代码

美团一直使用Git做代码版本控制,开发完之后提交pull request到目标分支,审查通过后即可合并。所以对于单次提交,可将新增的代码定义为:

  1. 本地工作目录中还没提交到暂存区的代码。
  2. 已经提交到暂存区的代码。
  3. 上次merge以后到还没有merge的commit中的代码。

如下图所示:

得到新增代码的定义以后,如何得到这些文件中真正新增的代码:

  • 把当前检测变化的Java文件放到一个临时目录A中。
  • 分别查看第一步找到的文件在最近一个merge的commit中的文件,并放到临时目录B中。

为了充分测试修改的代码,这里把方法作为最小测试单元(新增和修改的方法),即使是修改了方法中的某一行代码也认为这个方法发生了变化。如何准确定位到哪些方法发生了变化?我们通过抽象语法树来实现。

抽象语法树

所谓抽象语法树,就是源代码的抽象语法结构的树状表现形式,树上的每一个节点代表源代码中的一种结构。

下面通过Android Studio的JDT-View插件来表示一个简单的抽象语法树结构,左边是源码,右边是解析完以后的抽象语法结构:

后续语法树分析的实现通过Eclipse的JDT来完成。用JDT主要解决两个问题:

  • 定位哪些方法发生了变化。
  • 把JDT分析出的结果转化为合适的数据结构,方便后面做增量注入。

第一个问题比较容易解决,分别生成两组Java文件(上一部分结尾得到的两组文件A、B)的语法树,并对方法(去掉注释和空行)进行MD5,MD5不同的方法,便认为该方法在这次提交中发生了变化。

对于第二个问题,主要的难点在于通过JDT得到的方法定义和通过ASM(后面字节码注入通过ASM来实现)得到的方法定义不同,这二者最大的区别是JDT无法直接得到内部类、匿名内部类、Lambda表达式的ClassName,所以需要在语法树分析时把方法对应的ClassName转化成字节码对应的ClassName。字节码生成内部类和RetroLambda ClassName的规则如下:

  • 匿名内部类:…$Index。
  • 普通内部类、静态内部类:…$InnerClassName。
  • RetroLambda表达式:…$$Lambda$Index。

具体如何处理呢?JDT在分析Java文件时有几个关键的函数:

  • visit(MethodDeclaration method):访问普通方法的定义。
  • visit(AnonymousDeclaration method):访问匿名内部类的定义。
  • endVisit(AnonymousDeclaration method):结束匿名内部类的定义。
  • visit(TypeDeclaration node):访问普通类定义。
  • endVisit(TypeDeclaration node):结束普通类的定义。
  • visit(LambdaExpress node):访问Lambda表达式的定义。

同时在解析源文件时会按照源码定义顺序来访问各个节点。对于以上情况,只需要按照入栈和出栈的顺序来管理ClassName,就能和后面字节码得到的方法所匹配。

通过以上步骤,把每个方法的信息封装到MethodInfo中(后面注入和生成覆盖率报告时会用到该数据):

public String className;//hash package
public String md5;
public String methodName;
public List<String> paramList = new ArrayList<>();
public String methodBody;
public boolean isLambda;         //标识是否是Lambda表达式方法
public int lambdaNumInClass;     //同一个Class中此lambda表达式是第几个. 从1开始.
public int totalLambdaInClass;   //同一个Class中lambda表达式的总数
public String lambdaParent;      //lambda表达式的父节点
public boolean isLambdaInAnonymous; //标识lambda表达式是否位于内部类中
public boolean isAnonymousClass; //标识是否是内部类方法

生成代码的覆盖率报告,首先想到的就是JaCoCo,下面分别介绍一下JaCoCo的原理和我们所做的改造。

JaCoCo概述

JaCoCo包含了多种维度的覆盖率计数器:指令级计数器(C0 coverage)、分支级计数器(C1 coverage)、圈复杂度、行覆盖、方法覆盖、类覆盖。其覆盖率报告的示例如下:

  • 绿色:表示行覆盖充分。
  • 红色:表示未覆盖的行。
  • 黄色棱形:表示分支覆盖不全。
  • 绿色棱形:表示分支覆盖完全。

注入原理

JaCoCo主要通过代码注入的方式来实现上面覆盖率的功能。JaCoCo支持的注入方式如下图(图片出自这里)所示:

包含了几种不同的收集覆盖率信息的方法,每个方法的实现都不太一样,这里主要关心字节码注入这种方式(Byte Code)。Byte Code包含Offline和On-The-Fly两种注入方式:

  • Offline:在生成最终的目标文件之前,对Class文件进行插桩,生成最终的目标文件,执行目标文件以后得到覆盖执行结果,最终生成覆盖率报告。
  • On-The-Fly:JVM通过-javaagent指定特定的Jar来启动Instrumentation代理程序,代理程序在ClassLoader装载一个class前先判断是否需要对class进行注入,对于需要注入的class进行注入。覆盖率结果可以在JVM执行代码的过程中完成。

可以看到,On-The-Fly因为要修改JVM参数,所以对环境的要求比较高,为了屏蔽工具对虚拟机环境的依赖,我们的代码注入主要选择Offline这种方式。

Offline的工作流程:

  1. 在生成最终目标文件之前对字节码进行插桩。
  2. 运行测试代码,得到运行时数据。
  3. 根据运行时数据、生成的class文件、源码生成覆盖率报告。

通过一张图来形象地表示一下:

如何实现代码注入呢?举个例子说明一下:

JaCoCo通过ASM在字节码中插入Probe指针(探测指针),每个探测指针都是一个BOOL变量(true表示执行、false表示没有执行),程序运行时通过改变指针的结果来检测代码的执行情况(不会改变原代码的行为)。探测指针完整插入策略请参考Probe Insertion Strategy

增量注入

介绍完JaCoCo注入原理以后,我们来看看如何做到增量注入:

JaCoCo默认的注入方式为全量注入。通过阅读源码,发现注入的逻辑主要在ClassProbesAdapter中。ASM在遍历字节码时,每次访问一个方法定义,都会回调这个类的visitMethod方法 ,在visitMethod方法中再调用ClassProbeVisitor的visitMethod方法,并最终调用MethodInstrumenter完成注入。部分代码片段如下:

@Override
public final MethodVisitor visitMethod(final int access, final String name,
      final String desc, final String signature, final String[] exceptions) {
   final MethodProbesVisitor methodProbes;
   final MethodProbesVisitor mv = cv.visitMethod(access, name, desc,
         signature, exceptions);
   if (mv == null) {
      methodProbes = EMPTY_METHOD_PROBES_VISITOR;
   } else {
      methodProbes = mv;
   }
   return new MethodSanitizer(null, access, name, desc, signature,
         exceptions) {
      @Override
      public void visitEnd() {
         super.visitEnd();
         LabelFlowAnalyzer.markLabels(this);
         final MethodProbesAdapter probesAdapter = new MethodProbesAdapter(
               methodProbes, ClassProbesAdapter.this);
         if (trackFrames) {
            final AnalyzerAdapter analyzer = new AnalyzerAdapter(
                  ClassProbesAdapter.this.name, access, name, desc,
                  probesAdapter);
            probesAdapter.setAnalyzer(analyzer);
            this.accept(analyzer);
         } else {
            this.accept(probesAdapter);
         }
      }
   };
}

看到这里基本上已经知道如何去修改JaCoCo的源码了。继承原有的ClassInstrumenter和ClassProbesAdapter,修改其中的visitMethod方法,只对变化了方法进行注入:

@Override
public final MethodVisitor visitMethod(final int access, final String name,
                                       final String desc, final String signature, final String[] exceptions) {
    if (Utils.shoudHackMethod(name,desc,signature,changedMethods,cv.getClassName())) {
        ...
    } else {
        return  cv.getCv().visitMethod(access, name, desc, signature, exceptions);
    }
}

生成增量代码的覆盖率报告

和增量注入的原理类似,通过阅读源码,分别需要修改Analyzer(只对变化的类做处理):

@Override
public void analyzeClass(final ClassReader reader) {
  if (Utils.shoudHackMethod(reader.getClassName(),changedMethods)) {
        ...
    } 
}

和ReportClassProbesAdapter(只对变化的方法做处理):

@Override
public final MethodVisitor visitMethod(final int access, final String name,
                                       final String desc, final String signature, final String[] exceptions) {
    if (Utils.shoudHackMethod(name, desc, signature, changedMethods, this.className)) {
        ...
    } else {
        return null;
    }
}

这样就能生成新增代码的覆盖率报告。如下图所示本次commit只修改了FoodPoiDetailActivity的onCreate和initCustomTitle这两个方法,那么覆盖率只涉及这些修改了的方法:

JDT vs ASM

在上面增量注入和生成增量代码覆盖率报告时都会去判断当前方法是否应该被处理。这里分别对比JDT和ASM解析结果中的className、methodName、paramList来判断当前方法是否需要被注入,部分代码片段:

public static boolean shoudHackMethod(String methodName, String desc, String signature, HashSet<MethodInfo> changedMethods, String className) {
    Map<String, List<String>> changedLambdaMethods = getChangedLambdaMethods(changedMethods);
    List<String> changedLambdaMethodNames = changedLambdaMethods.get(className.replace("/", "."));
    updateLambdaNum(methodName, className);
    int indexMethods = 0;
    outer:
    for (; indexMethods < changedMethods.size(); indexMethods++) {
        MethodInfo methodInfo = changedMethods[indexMethods]
        if (methodInfo.className.replace(".", "/").equals(className)) {
            if (methodName.startsWith('lambda$') && methodInfo.isLambda
                    && changedLambdaMethodNames != null && changedLambdaMethodNames.size() > 0) {
                //两者方法名相等
                if (methodInfo.methodName.equals(methodName)) {
                    changedLambdaMethodNames.remove(methodInfo.methodName)
                    return true;

                } else if (!changedLambdaMethodNames.contains(methodName)) {
                    //两者方法名不等,且不包含在改变的lambda方法中,通过加载顺序来判断
                    int lastIndex = methodInfo.methodName.lastIndexOf('$');
                    if (lastIndex <= 0) {
                        continue;
                    }
                    String tmpMethodName = methodInfo.methodName.substring(0, lastIndex);
                    if (tmpMethodName.equals(sAsmMethodInfo.methodName)
                            && (methodInfo.lambdaNumInClass == (methodInfo.totalLambdaInClass - sAsmMethodInfo.lambdaNumInClass + 1) || judgeSoleLambda(changedMethods, methodInfo, methodName, className.replace("/", ".")))) {
                        changedLambdaMethodNames.remove(methodInfo.methodName)
                        return true;
                    }
                }
            } else {
                if (methodInfo.methodName.equals(methodName) ||
                        (!methodInfo.methodBody.trim().equals("{}") && methodName.equals("<init>") && methodInfo.methodName.equals(methodInfo.className.split("\\.|\\\$")[methodInfo.className.split("\\.|\\\$").length - 1]))) {
                    if (signature == null) signature = desc;
                    TraceSignatureVisitor v = new TraceSignatureVisitor(0);
                    new SignatureReader(signature).accept(v);
                    String declaration = v.getDeclaration();
                    int rightBrace = declaration.indexOf("(");
                    int leftBrace = declaration.lastIndexOf(")");
                    if (rightBrace > 0 && leftBrace > rightBrace) {
                        //只取形参
                        declaration = declaration.substring(rightBrace + 1, leftBrace);
                    }
                    //勿用\\[\\]作为分隔符, 否则数组形参不可区分
                    String paraStr = declaration.replaceAll("[(){}]", "");
                    if (paraStr.length() > 0) {
                        String[] parasArray = getAsmMethodParams(paraStr.split(","), className, methodInfo.paramList);
                        List<String> paramListAst = getAstMethodParams(methodInfo.paramList);
                        if (parasArray.length == paramListAst.size()) {
                            for (int i = 0; i < paramListAst.size(); i++) {
                                //将< > . 作为分隔符
                                String[] methodInfoParamArray = paramListAst.get(i).split("<|>|\\.");
                                for (String param : methodInfoParamArray) {
                                    if (!parasArray[i].contains(param) ||
                                            (parasArray[i].contains(param) && parasArray[i].contains("[]") && !param.endsWith("[]"))) {
                                        //同类名、同方法名、同参数长度, 参数类型不一致(或者 比较相等, 但class中是数组, 而源码中不是数组) 跳转到 outer循环开始处
                                        continue outer;
                                    }
                                }
                            }
                        } else {
                            continue;
                        }
                    }
                    if (methodInfo.isLambda && changedLambdaMethodNames != null) {
                        changedLambdaMethodNames.remove(methodInfo.methodName)
                    }
                    return true;
                }
            }
        }
    }
    return false;
}


自动注入

整个工具通过Gradle插件的形式加入到项目中,只需要简单配置即可使用,在生成DEX之前完成增量代码的注入,同时为了不影响线上版本,该插件只在Debug模式下生效。

自动获取运行时数据

刚才讲JaCoCo原理的时候提到,需要运行时数据才能生成覆盖率报告。代码中通过反射执行下面的函数来获取运行时数据,并保存到当前执行代码的设备中:

org.jacoco.agent.rt.RT.getAgent().getExecutionData(false)

由于生成报告时需要用到运行时数据,为了生成的覆盖率报告更准确、开发同学用起来更方便,分别在如下时机把运行时数据保存到当前设备中:

  • 每个页面执行onDestory时。
  • 程序发生崩溃时。
  • 收到特定广播(一个自定义的广播,在执行生成覆盖率报告的task前发送)时。

并在生成覆盖率报告之前把设备中的运行时数据同步到本地开发环境中。

上面可以看到,因为获取时机比较多,可能会得到多份运行时数据,对于这些数据,可以通过JaCoCo的mergeTaskClassId相同的运行时数据进行merge。如下图所示,JaCoCo会对ClassId相同的运行时数据进行merge,并对相同位置的probe指针取或:

自动部署Pre-Push脚本

为了开发者在提交代码之前能够自动生成覆盖率报告,我们在插件apply阶段动态下发一个Pre-Push脚本到本地项目的.git目录。在push之前生成覆盖率报告,同时对于覆盖率小于一定值(默认95%,可自定义)的提交提示并报警:

整个工具通过Gradle插件的形式部署到项目中,在项目编译阶段完成新增代码的查找和注入,在最终push代码之前获取当前设备的运行时数据,然后生成覆盖率报告,并把覆盖率低于一定值(默认是95%)的提交abort掉。

最后通过一张完整的图来看下这个工具的工作流程:

上述是我们在保障开发质量方面做的一些探索和积累。通过保障开发阶段增量代码的自测覆盖率,让开发者充分检验开发效果,提前发现逻辑缺陷,将风险前置。保障开发质量的道路任重而道远, 我们可以通过良好的测试覆盖率、持续完善单测、改善代码框架、规范开发流程等等多种维度相辅相成、共同推进。

  1. JaCoCo-Source-Code
  2. Java代码覆盖率工具JaCoCo-原理篇

本文三位作者均来自美团的到店餐饮技术部信息与交易技术中心。

武智,Android高级开发工程师,2013年7月校招加入美团,目前负责维护大众点评App的美食频道。

莹莹,2015年校招加入美团,主要参与大众点评美食频道的日常开发工作,专注于通过工具自动化地提高开发效率和质量。

周佳,2016年校招加入美团,主要参与大众点评美食频道的日常开发工作。

到店餐饮技术部交易与信息技术中心,负责美团美食用户端业务,服务于数以亿计用户,通过更好的榜单、真实的评价和完善的信息为用户提供更好的决策支持,致力于提升用户体验;同时承载所有餐饮商户端线上流量,为餐饮商户提供多种营销工具,提升餐饮商户营销效率,最终达到让国人“Eat Better、Live Better”的美好愿景!我们的团队包含且不限于Android、iOS、FE、Java、PHP等技术方向,已完备覆盖前后端技术栈。只要你来,就能点亮全栈开发技能树。诚挚欢迎投递简历至chenhongbing#meituan.com。

【思考题】

本文为大家介绍的工具基本上可以解决新增代码没有覆盖导致的问题。但开发过程中还会有一些因为数据、状态错误导致的问题,对于这类问题,通过什么工具可以及时的发现并解决?日常测试过程中用到测试数据是否被有效的利⽤和积累,是否能利用大数据相关的技术完善新时代的测试体系?