天天看点

EasyMock+PowerMock+Cobertura实现单元测试

EasyMock+PowerMock+Cobertura实现单元测试

EasyMock:单元测试就是对软件的一个单元进行隔离测试,然而大多数软件的各个单元并不是孤立,它们相互协作,有着千丝万缕的联系,因此为了对一个单元进行测试,我们就必须对这个单元依赖的其他单元进行模拟。

使用过程:

加入依赖:

<dependency>
   <groupId>org.easymock</groupId>
   <artifactId>easymock</artifactId>
   <version>3.2</version>
          <exclusions>
              <exclusion>
                  <artifactId>objenesis</artifactId>
                  <groupId>org.objenesis</groupId>
              </exclusion>
          </exclusions>
</dependency>
           

一般我们先创建一个Mock对象:

然后录制这个对象的行为:设置方法的参数,并且设置返回值,

激活录制:

之后我们执行需要测试的业务代码并检验代码是否返回正确的结果:

验证整个过程中,Mock对象是否完成了record阶段的设定,mock方法在测试过程中没有得到执行就会报错:

EasyMock遇到的问题:

*在mock完对象后,调用测试方法时报错空指针异常:

分析:在测试方法中需要调用mock的对象,而测试类并没有扫描配置文件,mock的对象也没有注入spring工厂中。所以调用时mock对象为null->空指针异常

解决:在测试方法的最后为mock对象写一个set方法,在录制完mock对象的行为后,把该对象set进测试主体对象中。

*java.lang.AssertionError: Unexpected method call …

在用

req.setSource("01");
   req.setProjectId("11"); 
   EasyMock.expect(projectBiz.getProject(req)).andReturn(result);
           

录制对象行为时,不要在方法入参时填入具体的参数,而是以

这样更加灵活的参数匹配

*遇到void方法时需要使用EasyMock.expectLastCall();

模板:redisUtil为mock出的对象

redisUtil.releaseLock(EasyMock.anyString());
     EasyMock.expectLastCall();
           

PowerMock:PowerMock是一个扩展了其它如EasyMock等mock框架的、功能更加强大的框架。PowerMock使用一个自定义类加载器和字节码操作来模拟静态方法,构造函数,final类和方法,私有方法,去除静态初始化器等等。通过使用自定义的类加载器,简化采用的IDE或持续集成服务器不需要做任何改变。熟悉PowerMock支持的mock框架的开发人员会发现PowerMock很容易使用,因为对于静态方法和构造器来说,整个的期望API是一样的。PowerMock旨在用少量的方法和注解扩展现有的API来实现额外的功能。

使用过程:

加入依赖:

<dependency>
   <groupId>org.powermock</groupId>
   <artifactId>powermock-module-junit4</artifactId>
   <version>1.4.12</version>
          <exclusions>
              <exclusion>
                  <artifactId>javassist</artifactId>
                  <groupId>javassist</groupId>
              </exclusion>
          </exclusions>
          
</dependency>
<dependency>
   <groupId>org.powermock</groupId>
   <artifactId>powermock-api-mockito</artifactId>
   <version>1.4.12</version>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-easymock</artifactId>
    <version>1.4.12</version>
</dependency>
           

添加两个注解,@PrepareForTest中的类为模板静态方法所在的类,该注解也可以放在具体的方法上:

@RunWith(PowerMockRunner.class)  
 @PrepareForTest( { YourClassWithEgStaticMethod.class })
           

Mock静态方法:

录制行为:

PowerMock遇到的问题:

第一个问题:java.lang.ClassFormatError

解决包依赖,整理maven的依赖关系,理清复杂的依赖关系 ,清理一些多余的依赖包,纠正错误的依赖关系;

第二个问题:java.lang.VerifyError: Inconsistent stackmap frames at branch target

jvm参数 :java.lang.VerifyError: Inconsistent stackmap frames at branch target ?

原因-PowerMock中为支持对构造函数的测试,借助于Javassist实现对字节码的操作。但是从Java 6开始引入的Stack Map Frames特性与Javassist不兼容。在Java 6中该Stack Map Frames还是可选的。但是到了Java 7,该Stackmap Frames已经是默认使用的,所以不兼容问题导致了该异常。

修改JVM 7参数:-XX:-UseSplitVerifier

->配置jvm运行参数屏蔽该特性:Run-Edit configurations-VM options选中方法后填入 -XX:-UseSplitVerifier

第三个问题:Could not reconfigure JMX java.lang.LinkageError

->忽略powermock异常:在类上加注释:@PowerMockIgnore({"javax.management."})

---->问题解决

*PowerMock测试时待测试类初始化时自带静态方法导致初始化报错:

class A{
private HttpClientPool = HttpClientPool.getInstance();
}
           

->在类上加注释:

@RunWith(PowerMockRunner.class)

@PrepareForTest({MongoHandler.class,HttpClientPool.class}) //getInstance()所在的类

@PowerMockIgnore({“javax.management.*”})

mock该类:

PowerMockito.mockStatic(HttpClientPool.class);

---->问题解决

PoewrMock定义方法行为时,mock的静态方法返回值都为空:

->发现在入参中使用了Easy.any(),可能是导致该问题的原因。

又重新尝试后,发现只要静态方法参数在默认情况下不匹配都会返回null;

然后开始查找入参的任意匹配方式:

为了实现更加灵活的参数匹配尝试后发现可以使用例如Mockito.anyString()这样的入参方式问题得以解决,就类似于EasyMock.anyString();

Mockito.any(BasicDBObject.class)类似于EasyMock.anyObject(TicketProjectReq.class)

*多次调用返回不同值的问题

在测试projectListFromMongo时,在第一次查询数量为0时,会再次调用mock的方法再次查询,为了使第二次查询的时候返回数量>0:

->查找使得mock方法多次调用返回不同值的方式:

PowerMockito.when(MongoHandler.getquerProjectResult()).thenReturn(bo).thenReturn(bo_b);

这样在第一次调用getquerProjectResult()时返回bo,第二次调用getquerProjectResult()时返回bo_b;

*static void方法的mock的问题:

这实际上是一个static void方法的mock,对于这种方法,我们不需要对具体的方法做mock,只需要mock整个类,这样调用该方法的时候就不会跑真正的方法。其实对于static void方法最好的是什么都不做。

-> PowerMockito.mockStatic(TransactionSynchronizationManager.class);

Cobertura :Cobertura 是一种开源工具,它通过检测基本的代码,并观察在测试包运行时执行了哪些代码和没有执行哪些代码,来测量测试覆盖率。除了找出未测试到的代码并发现 bug 外,Cobertura 还可以通过标记无用的、执行不到的代码来优化代码,还可以提供 API 实际操作的内部信息。

使用过程:

->在pom中添加:

<plugin>  
         <groupId>org.codehaus.mojo</groupId>  
         <artifactId>cobertura-maven-plugin</artifactId>  
         <version>2.5.1</version>  
 </plugin> 
           

->将项目重新打包

->点击maven的Excute Maven Goal,输入命令:mvn cobertura:cobertura

->完成后,会在项目的目录target下有个site文件夹,我们打开里面的index页面,可以看到详细的覆盖率以及代码执行次数等。

Cobertura 遇到的问题:

*生成的测试覆盖率报告中,发现覆盖率为零:

阅读日志理解:

[INFO] >>> cobertura-maven-plugin:2.5.1:cobertura (default-cli) > [cobertura]test @ ticketDubbo-internet >>>
  ->cobertura-maven-plugin绑定到了maven生命周期test上.
  [INFO] --- cobertura-maven-plugin:2.5.1:instrument (default-cli) @ ticketDubbo-internet ---
  [INFO] Instrumentation was successful.
           

->完成项目源码编译后,执行了cobertura:instrument,对项目源码做了标记.

问题可能出在这里:没有复制、编译测试资源,测试被跳过

[INFO] NOT adding cobertura ser file to attached artifacts list.
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ ticketDubbo-internet ---
[INFO] Not copying test resources
[INFO] 
[INFO] --- maven-compiler-plugin:2.5.1:testCompile (default-testCompile) @ ticketDubbo-internet ---[INFO] Not compiling test sources
[INFO] 
[INFO] --- maven-surefire-plugin:2.7.2:test (default-test) @ ticketDubbo-internet ---
[INFO] Tests are skipped.
           

->尝试运行时使用网上搜索的方法cobertura:cobertura -Dmaven.test.skip=false 也没有成功,还是 Not compiling test sources;

->仔细阅读日志发在运行命令cobertura:cobertura过程中,它会使用maven-compiler-plugin去编译代码,使用maven-surefire-plugin去执行测试资源。

->maven编译时:maven-compiler-plugin用来编译Java代码,src/main/java和src/test/java 两个目录中的所有*.java文件会分别在comile和test-comiple阶段被编译,编译结果分别放到了target/classes和target/test-classes目录中

->继续看日志:

[INFO] --- maven-compiler-plugin:2.5.1:compile (default-compile) @ ticketDubbo-internet ---
[INFO] Compiling 59 source files to E:\migumusic\ecosp-ticket-center-internet\ticketDubbo-internet\target\classes
           

cobertura编译项目时只是进行了comile阶段没有进行test-comiple阶段,target/也没有发现test-classes,所以这应该是没有编译单元测试导致了结果中测试覆盖率为0的原因。

->尝试在maven窗口手动运行compiler:testCompile:

[INFO] --- maven-compiler-plugin:2.5.1:testCompile (default-cli) @ ticketDubbo-internet ---
[INFO] Not compiling test sources
           

发现还是没有编译编译测试代码。

->所以应该和maven-compiler-plugin这插件有关,然后同时对maven-surefire-plugin、maven-compiler-plugin两个插件进行学习

->maven-compiler-plugin插件了解:

maven是个项目管理工具,如果我们不告诉它我们的代码要使用什么样的jdk版本编译的话,它就会用maven-compiler-plugin默认的jdk版本来进行处理,这样就容易出现版本不匹配的问题,以至于可能导致编译不通过的问题。

例如代码中要是使用上了jdk1.7的新特性,但是maven在编译的时候使用的是jdk1.6的版本,那这一段代码是完全不可能编译成.class文件的。

<source>1.6</source> <!-- 源代码使用的开发版本 -->
<target>1.6</target> <!-- 需要生成的目标class文件的编译版本 -->
<!-- 一般而言,target与source是保持一致的,但是,有时候为了让程序能在其他版本的jdk中运行(对于低版本目标jdk,源代码中需要没有使用低版本jdk中不支持的语法),会存在target不同于source的情况 -->
<encoding>UTF8</encoding>
windows默认使用GBK编码,java项目经常编码为utf8,也需要在compiler插件中指出,否则中文乱码可能会出现编译错误。  
           

->maven-surefire-plugin插件了解:

Maven本身并不是一个单元测试框架,Java世界中主流的单元测试框架为JUnit和TestNG。Maven所做的只是在构建执行到特定生命周期阶段的时候,通过插件来执行JUnit或者TestNG的测试用例。这一插件就是maven-surefire-plugin,可以称之为测试运行器(Test Runner),他能很好的兼容JUnit 3、JUnit 4以及TestNG。生命周期阶段需要绑定到某个插件的目标才能完成真正的工作,test阶段正是与maven-surefire-plugin的test目标相绑定了,这是一个内置的绑定。

在默认情况下,maven-surefire-plugin的test目标会自动执行测试源码路径(默认为src/test/java/)下所有符合一组命名模式的测试类。这组模式为:

*/Test.java:任何子目录所有命名以Test开头的Java类。

***TestCase.java:任何子目录下所有命名以TestCase结尾的Java类。

只要将测试类按上述模式命名,Maven就能自动运行他们,用户也就不再需要定义测试集合(TestSuite)来聚合测试用例(TestCase)。关于模式需要注意的是,以Test结尾的测试类是不会得以自动执行的。

->回来后在parent的pom中找到了这两个插件,都发现了true元素。

其中maven-compiler-plugin中的

<skin>true</skin>

表示不执行测试用例,也不编译测试用例类。maven-surefire-plugin中的

<skin>true</skin>

表示不执行测试用例;

->于是它们都改为false,重新install,再次运行cobertura:cobertura时发现日志中已经有了test的编译和运行信息:

[INFO] --- maven-surefire-plugin:2.7.2:test (default-test) @ ticketDubbo-internet --- 
[INFO] Surefire report directory: E:\migumusic\ecosp-ticket-center-internet\ticketDubbo-internet\target\surefire-reports 
------------------------------------------------------- 
 T E S T S 
------------------------------------------------------- 
Running com.sitech.miso.ecosp.ticketdubbo.core.biz.OrderBizImplTest 
Tests run: 4, Failures: 0, Errors: 4, Skipped: 0, Time elapsed: 0.09 sec <<< FAILURE! 
Running com.sitech.miso.ecosp.ticketdubbo.core.biz.ProjectBizImplTest 
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.378 sec <<< FAILURE! 
Running com.sitech.miso.ecosp.ticketdubbo.core.biz.TicketBizImplTest 
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.182 sec <<< FAILURE! 
Running com.sitech.miso.ecosp.ticketdubbo.core.biz.YlHttpBizImplTest 
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.136 sec <<< FAILURE! 
Running com.sitech.miso.ecosp.ticketdubbo.core.service.OrderServiceImplTest 
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.114 sec <<< FAILURE! 
Running com.sitech.miso.ecosp.ticketdubbo.core.service.TicketServiceImplTest 
Tests run: 7, Failures: 0, Errors: 7, Skipped: 0, Time elapsed: 0.473 sec <<< FAILURE! 
           

->但是又发现运行中报错了,查看报错报告是之前遇到过的java.lang.VerifyError: Inconsistent stackmap frames at branch target

->解决该问题的办法是修改方法运行时的VM options

->查找在maven-surefire-plugin运行单元测试时修改VM options的方式:

尝试1:直接修改idea的VM Options:Help->Editcustom 加入-XX:-UseSplitVerifier,无效

尝试2:因为它是用maven的插件来跑的单元测试,修改maven-Runner中的VMoptions,无效

尝试3:直接修改maven-surefire-plugin插件的配置参数,搜索了很久终于找到,在pom的配置中加入:

<argLine>-XX:-UseSplitVerifier</argLine>

,再次重新运行,日志发现添加的VM参数已经被带入,Test运行成功。

运行cobertura:cobertura,打开报告,覆盖率生成成功。

*手动运行单元测试时正常,但是在maven-surefire-plugin运行单元测试时出现:java.lang.OutOfMemoryError: PermGen space

->PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,这块内存主要是被JVM存放Class和Meta信息的,Class在被Loader时就会被放到PermGen space中, 它和存放类实例(Instance)的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的应用中有很多CLASS的话,就很可能出现PermGen space错误,而maven-surefire-plugin运行单元测试时是单独fork一个进程去做单元测试,当测试资源达到一定量时易导致内存溢出。

->需要手动设置MaxPermSize的大小:

修改该插件的运行参数,在pom配置文件中加入

<argLine>-XX:-UseSplitVerifier -XX:MaxPermSize=1024m</argLine>

后解决问题。

继续阅读