• 测试覆盖率Emma工具使用


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

    ##查看测试报告

    ###本地测试查看:
    生成的报告是以html存储,默认的位置是${项目目录}/target/site/emma,打开index.html可以查看。
    里面有类覆盖率,方法覆盖率,块覆盖率,行覆盖率,
    emma_3
    选中其中的java文件还可以查看具体的代码覆盖率
    emma_4
    绿色为有测试的,红色的是测试未覆盖的。

    Jenkins 测试查看:

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

    #工作原理
    emma现在有两种工作方式,on-the-fly模式和offline模式:
    emma_6
    On the fly 模式往加载的类中加入字节码,在程序运行中,用 EMMA 实现的classLoader 替代应用默认的 Custom classLoader,动态加载类文件,并向类中加入一些统计测试的字节码,这样运行结束后,测试数据也就通过这些临时加入的字节码分析出来。
    Offline 模式在类被加载前,在编译生成的class文件中加入字节码。
    On the fly 模式比较方便,缺点也比较明显:
    emma_11
    它不能为被 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工作时主要运行以下几个步骤
    emma_7

    1. 字节码插装并生成插装的元信息文件coverage.em
    2. 运行测试
    3. 每次当 JVM 停止时,内存中记录的执行信息将被清除并被保存到 “coverage.ec” 的文件中。
    4. 生成测试报告。
      ##插装字节码
      emma执行是最重要的就是插装字节码:
      emma_8
      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_9
    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 + "]");
        }
    

    emma_10

    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测试报告是,只用了生成概要覆盖信息、源文件测试覆盖信息、包测试覆盖信息的方法。

    参考资料

  • 相关阅读:
    FFT 优化和任意模数 FFT
    构造 DFT :某少女附中的体育课 题解
    多项式操作
    String主要方法
    我的第一篇java笔记
    个人作业——软件评测
    软件工程实践2019第五次作业
    软件工程实践2019第四次作业
    软件工程实践2019第三次作业
    软件工程实践2019第二次作业
  • 原文地址:https://www.cnblogs.com/danqiu/p/6125806.html
Copyright © 2020-2023  润新知