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>
后解决问题。