Emma使用与分析

#什么是Emma

EMMA 是一个开源、面向 Java 程序测试覆盖率收集和报告工具。它通过对编译后的 Java 字节码文件进行插装,在测试执行过程中收集覆盖率信息,并通过支持多种报表格式对覆盖率结果进行展示。 EMMA 所使用的字节码插装不仅保证 EMMA 不会给源代码带来“脏代码”,还确保 EMMA 摆脱了源代码的束缚,这一特点使 EMMA 应用于功能测试成为了可能。

#如何使用
emma现在可以通过命令行,ant,maven,Jenkins等方式使用,这里只介绍通过maven和Jenkins来集成emma测试。

##在Maven中的使用
直接运行mvn emma:emma,即可。
maven集成emma,需要两个插件,maven-surefire-plugin和emma-maven-plugin,如果之前没有安装,那么maven会自动下载这两个插件。
emma依赖于surefire的配置,默认执行src/test/java的junit测试。为了方便使用,最好在自己的pom里配置maven-surefire-plugin插件。

            <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.8.1</version>
<configuration>
<skipTests>false</skipTests>
<junitArtifactName>junit:junit</junitArtifactName>
<includes>
<include>**/*Test.java</include>
</includes>
<excludes>
<exclude>**/*_Roo_*</exclude>
</excludes>
</configuration>
</plugin>

这样指定maven-surefire-plugin的版本为2.8.1,false不跳过测试,/*Test.java只测试以Test.java为文件名结尾的文件,而且不测试/Roo文件名包含Roo的文件。更多的配置可以去查看maven-surefire-plugin的配置说明http://maven.apache.org/plugins/maven-surefire-plugin/。

##在Jenkins中的使用
在Jenkins系统管理的插件管理页面,添加Jenkins Emma plugin插件。
在项目配置中,加入emma:emma即可使用。


因为测试需要很长时间,而package命令会自动执行测试,所以有时候我们不想所有项目都测试。可以使用如下方案:系统配置两个分支,A分支用于开发,B分支用于上线。我们希望只要A分支进行emma测试,而在B分支不用测试方便快速上线。配置如下
在项目的pom.xml中,false默认不跳过测试,在B项目中配置clean -U compile package -Dmaven.test.skip=true,用来跳过测试。

##查看测试报告

###本地测试查看:
生成的报告是以html存储,默认的位置是${项目目录}/target/site/emma,打开index.html可以查看。
里面有类覆盖率,方法覆盖率,块覆盖率,行覆盖率,

选中其中的java文件还可以查看具体的代码覆盖率

绿色为有测试的,红色的是测试未覆盖的。

Jenkins 测试查看:

在项目主页中查看

这里会有项目的测试覆盖率曲线。x轴是版本变化,y轴是测试覆盖率。
点进图片进入本版本的详细测试报告。具体的形式和本地测试报告差不多,只是 jenkins测试报告没有具体的代码测试详情。

#工作原理
emma现在有两种工作方式,on-the-fly模式和offline模式:

On the fly 模式往加载的类中加入字节码,在程序运行中,用 EMMA 实现的classLoader 替代应用默认的 Custom classLoader,动态加载类文件,并向类中加入一些统计测试的字节码,这样运行结束后,测试数据也就通过这些临时加入的字节码分析出来。
Offline 模式在类被加载前,在编译生成的class文件中加入字节码。
On the fly 模式比较方便,缺点也比较明显:

它不能为被 boot class loader 加载的类生成覆盖率报告;而且,J2EE的classLoader和EMMA的classLoader都是同一类Custom classLoader,在j2ee项目启动过程中,必须选择应用容器(tomcat、Weblogic等等)相应的classLoader,从而无法使用emma的classLoader。同时,jenkins必须配合mvn的框架才能运行emma相关命令,而mvn框架只支持offline模式,所以如果想使用jenkins来做测试报告的话,就无法使用on the fly模式。在官方文档里也有说明:
As convenient as the on-the-fly mode is, in many cases it is not sufficient. For example, running a commercial J2EE container in a custom instrumenting classloader is practically impossible. Certain (bad) coding practices also fail for code executing in a custom classloader. 
This on-the-fly instrumentation mode is handy for light-weight testing of main() test methods, individual classes, and small- to- mid-size programs. emmarun also works well with Swing applications.

这时,我们只能求助于 Offline 模式。下面用maven的运行方式来介绍一下。

通过在maven中执行,我们可以看出emma工作时主要运行以下几个步骤

  1. 字节码插装并生成插装的元信息文件coverage.em
  2. 运行测试
  3. 每次当 JVM 停止时,内存中记录的执行信息将被清除并被保存到 “coverage.ec” 的文件中。
  4. 生成测试报告。
    ##插装字节码
    emma执行是最重要的就是插装字节码:

    emma循环调用handleFile()方法来遍历目录下所有以'.class'结尾的文件,然后使用 classParser类得到要插装的组件
ClassDef class_table () throws IOException
{
m_table = new ClassDef ();
magic ();
version (); if (DEBUG) System.out.println (s_line); constant_pool (); if (DEBUG) System.out.println (s_line); access_flags ();
this_class ();
super_class (); if (DEBUG) System.out.println (s_line); //得到所有接口
interfaces ();
if (DEBUG) System.out.println (s_line); //得到所有字段
fields ();
if (DEBUG) System.out.println (s_line); //得到所有方法
methods ();
if (DEBUG) System.out.println (s_line); //得到所有attribute
attributes ();
if (DEBUG) System.out.println (s_line); return m_tabl e;
}

offline模式的插装会生成全新的class文件,默认放在target/generated-classes下。以下是原java文件和插装后的class反编译的java文件。

public class EmmaMain2 {
private Logger logger = LoggerFactory.getLogger(this.getClass()); //junit调用publicTest()进行测试
public void publicTest(){
logger.info("this is a public method");
logger.info("我是分隔符------------------------------------------------------------------");
for(int i =1 ;i<10;i++){
privateTest();
if(i==4){
continue;
}
if(i==3){
break;
}
//永远不会执行到这一步,所以protectedTest()并没有被覆盖
if(i==5){
protectedTest();
return;
}
}
} protected void protectedTest(){
logger.info("this is a protected method");
} private void privateTest(){
logger.info("this is a private method");
}
}

插装字节码之后反编译的代码

public class EmmaMain2
{
private Logger logger = LoggerFactory.getLogger(getClass());
private static final boolean[][] $VRc;
private static final long serialVersionUID = -6204774612524021426L;
public EmmaMain2()
{
arrayOfBoolean[0] = true;
}
public void publicTest()
{
boolean[][] tmp3_0 = $VRc; if (tmp3_0 == null) tmp3_0; boolean[] arrayOfBoolean = $VRi()[1]; this.logger.info("this is a public method");
this.logger.info("我是分隔符------------------------------------------------------------------");
int i = 1; arrayOfBoolean[0] = true;
tmpTernaryOp = tmp3_0;
do
{
privateTest();
arrayOfBoolean[1] = true; if (i == 4) { arrayOfBoolean[2] = true;
} else
{
arrayOfBoolean[3] = true; if (i == 3) { arrayOfBoolean[4] = true;
break;
}
arrayOfBoolean[5] = true; if (i == 5) {
protectedTest(); arrayOfBoolean[6] = true;
return;
}
}
i++; arrayOfBoolean[7] = true; arrayOfBoolean[8] = true; } while (i < 10);
arrayOfBoolean[9] = true;
}
protected void protectedTest()
{
boolean[][] tmp3_0 = $VRc; if (tmp3_0 == null) tmp3_0; boolean[] arrayOfBoolean = $VRi()[2]; this.logger.info("this is a protected method");
arrayOfBoolean[0] = true;
}
private void privateTest()
{
boolean[][] tmp3_0 = $VRc; if (tmp3_0 == null) tmp3_0; boolean[] arrayOfBoolean = $VRi()[3]; this.logger.info("this is a private method");
arrayOfBoolean[0] = true;
}
static
{
boolean[] arrayOfBoolean = $VRi()[4];
arrayOfBoolean[0] = true;
}
private static boolean[][] $VRi()
{
boolean[][] tmp9_6 = (EmmaMain2.$VRc = new boolean[5]);
tmp9_6[0] = new boolean[1];
boolean[][] tmp15_9 = tmp9_6;
tmp15_9[1] = new boolean[10];
boolean[][] tmp22_15 = tmp15_9;
tmp22_15[2] = new boolean[1];
boolean[][] tmp28_22 = tmp22_15;
tmp28_22[3] = new boolean[1];
boolean[][] tmp34_28 = tmp28_22;
tmp34_28[4] = new boolean[1];
boolean[][] tmp40_34 = tmp34_28;
//将类信息加载到内存中。
RT.r(tmp40_34, "com/impulse/test/emma/EmmaMain2", -5598510326399570528L);
return tmp40_34;
}
}

反编译有些问题,但我们可以看出,emma在每个方法的入口和出口和转移指令之前如return、break、continue都加入了监测代码,并在最后把代码的执行情况通过RT.r()方法加载到内存的m_coverageMap中。

public static void r (final boolean [][] coverage, final String classVMName, final long stamp)
{
// note that we use class names, not the actual Class objects, as the keys here. This
// is not the best possible solution because it is not capable of supporting
// multiply (re)loaded classes within the same app, but the rest of the toolkit
// isn't designed to support this anyway. Furthermore, this does not interfere
// with class unloading.
final ICoverageData cdata = getCoverageData (); // need to use accessor for JMM reasons
// ['cdata' can be null if a previous call to dumpCoverageData() disabled data collection] if (cdata != null)
{
synchronized (cdata.lock ())
{
// TODO: could something useful be communicated back to the class
// by returning something here [e.g., unique class ID (solves the
// issues of class name collisions and class reloading) or RT.class
// (to prevent RT reloading)] cdata.addClass (coverage, classVMName, stamp);
}
}
}
public void addClass (final boolean [][] coverage, final String classVMName, final long stamp)
{
m_coverageMap.put (classVMName, new DataHolder (coverage, stamp));
}

所以当我们只测试publicTest()时,虽然publicTest()调用了protectedTest(),但由于我们通过条件语句的控制,使得protectedTest()永远不会被执行,因此在转移指令时加监控是必要的,我们可以在生成的报告中看出,

emma能够检测出那些虽然调用但没有执行到的代码。

##收集覆盖率信息
emma会检测jvm的运行情况,当通过命令行调用reset或者虚拟机停止(一般是测试完成时),emma会将测试的覆盖率信息通过 dumpCoverageData()方法导出成实体文件。默认为coverage-*.ec文件。

static void dumpCoverageData (final ICoverageData cdata, final boolean useSnapshot,
final File outFile, final boolean merge)
{
try
{
if (cdata != null)
{
// use method-scoped loggers everywhere in RT:
final Logger log = Logger.getLogger ();
final boolean info = log.atINFO (); final long start = info ? System.currentTimeMillis () : 0;
{
final ICoverageData cdataView = useSnapshot ? cdata.shallowCopy () : cdata; synchronized (Object.class) // fake a JVM-global critical section when multilply loaded RT's write to the same file
{
//在这里生者覆盖率信息文件,cdataView是CoverageData型,有一个重要的成员变量就是上面说的m_coverageMap
DataFactory.persist (cdataView, outFile, merge);
}
}
if (info)
{
final long end = System.currentTimeMillis (); log.info ("runtime coverage data " + (merge ? "merged into" : "written to") + " [" + outFile.getAbsolutePath () + "] {in " + (end - start) + " ms}");
}
}
}
catch (Throwable t)
{
// log
t.printStackTrace (); // TODO: do better chaining in JRE 1.4+
throw new RuntimeException (IAppConstants.APP_NAME + " failed to dump coverage data: " + t.toString ());
}
}

DataFactory.persist (cdataView, outFile, merge); cdataView是CoverageData型,有一个重要的成员变量就是上面说的m_coverageMap,没错,就是在这里把存在内存中的测试覆盖率信息持久化到文件中。

##生成测试报告
AbstractReportGenerator是个抽象工厂,根据参数不同而产生不同的 ReportGenerator。

public static IReportGenerator create (final String type)
{
if ((type == null) || (type.length () == 0))
throw new IllegalArgumentException ("null/empty input: type"); // TODO: proper pluggability pattern here if ("html".equals (type))
return new com.vladium.emma.report.html.ReportGenerator ();
else if ("txt".equals (type))
return new com.vladium.emma.report.txt.ReportGenerator ();
else if ("xml".equals (type))
return new com.vladium.emma.report.xml.ReportGenerator ();
else // TODO: error code
throw new EMMARuntimeException ("no report generator class found for type [" + type + "]");
}

public
abstract class AbstractItemVisitor implements IItemVisitor
{
// public: ................................................................
//概要覆盖信息
public Object visit (final AllItem item, final Object ctx)
{
return ctx;
}
//包测试覆盖信息
public Object visit (final PackageItem item, final Object ctx)
{
return ctx;
}
//源文件测试覆盖信息
public Object visit (final SrcFileItem item, final Object ctx)
{
return ctx;
}
//在html中没有
public Object visit (final ClassItem item, final Object ctx)
{
return ctx;
}
//在html没有
public Object visit (final MethodItem item, final Object ctx)
{
return ctx;
}
}

三种ReportGenerator都实现了IReportGenerator接口的process方法来到处报告,而process方法又分别调用了各种重载的visit()方法。当maven生成html测试报告是,只用了生成概要覆盖信息、源文件测试覆盖信息、包测试覆盖信息的方法。

参考资料

测试覆盖率Emma工具使用的更多相关文章

  1. 测试覆盖率工具EclEmma安装与使用

    此文来自于:https://www.cnblogs.com/cnsdhzzl/p/7638883.html EclEmma的简介 一个优秀的开源软件测试工具 eclipse的一个插件 能够对由 Jav ...

  2. 测试覆盖率工具:EclEmma

    测试覆盖率工具:EclEmma 2016-08-26 目录 1 测试覆盖率实现技术2 EclEmma介绍3 EclEmma测试覆盖率指标4 EclEmma安装5 示例项目介绍  5.1 创建项目  5 ...

  3. iOS 覆盖率检测原理与增量代码测试覆盖率工具实现

    背景 对苹果开发者而言,由于平台审核周期较长,客户端代码导致的线上问题影响时间往往比较久.如果在开发.测试阶段能够提前暴露问题,就有助于避免线上事故的发生.代码覆盖率检测正是帮助开发.测试同学提前发现 ...

  4. 精准测试与开源工具Jacoco的覆盖率能力大PK

    导读:本文根据实际使用情况,简要分析了精准测试和类Jacoco等传统白盒工具在设计理念.功能和应用场景的异同点,并阐述了覆盖率技术如何在新型企业开发体系中,发挥应有的重要作用. 覆盖率技术可以说是测试 ...

  5. [Node.js] Express的测试覆盖率

    原文地址:http://www.moye.me/2014/12/03/express_coverage/ 引子 有群友问到Express怎么做 单元测试/覆盖率测试,这是上篇所遗漏的,特此补上 Exp ...

  6. 使用 coverlet 查看.NET Core应用的测试覆盖率

    代码覆盖(Code coverage)是软件测试中的一种度量,描述程式中源代码被测试的比例和程度,所得比例称为代码覆盖率. Visual Studio 2017的企业版可以直接查看测试的代码覆盖率, ...

  7. 用 Cobertura 测量测试覆盖率

      尽管测试先行编程(test-first programming)和单元测试已不能算是新概念,但测试驱动的开发仍然是过去 10 年中最重要的编程创新.最好的一些编程人员在过去半个世纪中一直在使用这些 ...

  8. 让 Python 代码更易维护的七种武器——代码风格(pylint、Flake8、Isort、Autopep8、Yapf、Black)测试覆盖率(Coverage)CI(JK)

    让 Python 代码更易维护的七种武器 2018/09/29 · 基础知识 · 武器 原文出处: Jeff Triplett   译文出处:linux中国-Hank Chow    检查你的代码的质 ...

  9. 白盒测试笔记之:Junit 单元测试以及测试覆盖率

    单元测试: 单元测试是从代码层面验证代码的正确性. 一般考虑接口中的数据结构是否正确,变量的临界条件,如空字符串,空集合等. ​ Junit入门  参考: Junit 入门教程: https://ww ...

随机推荐

  1. (41)C#异步编程

    VS2010是经常阻塞UI线程的应用程序之一.例如用vs2010打开一个包含数百个项目的解决方案,可以要等待很长时间(感觉像卡死),自从vs2012情况得到了改善,项目在后台进行了异步加载. 一.同步 ...

  2. 转载:P2P技术原理及应用(2)

    转载allen303allen的空间 在Gnutella网络中存在以下问题: 冗余消息多,对带宽的消耗存在一定的浪费.Gnutella网络协议采用泛洪式(Flooding)消息传播机制,这种消息传播机 ...

  3. Codeforces A. Bear and Big Brother

    ...不行.这题之后.不做1000分以下的了.很耻辱   A. Bear and Big Brother time limit per test 1 second memory limit per t ...

  4. rocketMq---------相关命令

    搭建就不详细说了,cent7.x的系统,openJdk8,maven3.x,gradle4.10.2, git 1.8.3.1 直接下载相关的二进制压缩包,解压即用,方便. 下面看常用的管理命令 ro ...

  5. Interface Builder中的技巧

    在我工作中经常会遇到有人吐槽Xcode中的interface builder(以下简称IB)不好用的开发者.在我看来,IB是一个非常棒的可视化开发工具,可以非常快捷的设置UI控件的大部分常用属性.下面 ...

  6. openfire Android学习(三)----会议室创建、加入以及查询会议室中所有成员等

    openfire 中的会议室不像QQ群一样,不能保存那些离线用户,加入会议室后,一旦断开连接,就会离开会议室. 虽然如此,但如果要实现也不是不可能,我们可以自己做后台来保存,有兴趣的可以去试着实现一下 ...

  7. Majority Number

    题目描写叙述 链接地址 解法 算法解释 题目描写叙述 Given an array of integers, the majority number is the number that occurs ...

  8. LoadRunner多负载产生器

    Executive Summary : The following explains why it is necessary to have about 6 load generators when ...

  9. Odoo 养猪

    阶段 仔猪养成为肉猪 乳猪养成为仔猪 母猪繁殖乳猪     每一个饲养阶段,基本上在同一组笼舍,活动场所:进入到下一饲养阶段才会转换笼舍.在每一个饲养阶段,基本上经历以下过程 饲养 注射疫苗 饲养 投 ...

  10. Python生成器定义

    通过列表生成式,我们可以直接创建一个列表.但是,受到内存限制,列表容量肯定是有限的.而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素 ...