PowerMocker&Jacoco单元测试全解
第一章 pom文件的引入与各种坑
1. pom文件
<!-- 测试包 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- 主要是PowerMocker的工具包 -->
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<!-- jacocomaven插件包 -->
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
</dependency>
<!-- powermock -->
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
2. 插件
<build>
<plugins>
<!-- 主要的插件,我这个用的是离线模式,一定要用离线模式,
因为@PrepareForTest 会和传统模式冲突-->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
<executions>
<execution>
<id>default-instrument</id>
<goals>
<goal>instrument</goal>
</goals>
</execution>
<execution>
<id>default-restore-instrumented-classes</id>
<goals>
<goal>restore-instrumented-classes</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>${project.build.directory}/jacoco.exec</dataFile>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<systemPropertyVariables>
<!-- 生成的jacoco.exec文件就是统计覆盖率的文件-->
<jacoco-agent.destfile>
${project.build.directory}/jacoco.exec
</jacoco-agent.destfile>
</systemPropertyVariables>
</configuration>
</plugin>
<!-- 会生成大量的Test测试文件,但是不建议使用,后面会介绍插件使用-->
<plugin>
<groupId>com.github.houbb</groupId>
<artifactId>gen-test-plugin</artifactId>
<version>0.0.1</version>
<configuration>
<encoding>UTF-8</encoding>
<isOverwriteWhenExists>false</isOverwriteWhenExists>
<junitVersion>4</junitVersion>
</configuration>
</plugin>
</plugins>
</build>
########################################################################################
##
## Available variables:
## $entryList.methodList - List of method composites
## $entryList.privateMethodList - List of private method composites
## $entryList.fieldList - ArrayList of class scope field names
## $entryList.className - class name
## $entryList.packageName - package name
## $today - Todays date in yyyy-MM-dd format
##
## MethodComposite variables:
## $method.name - Method Name
## $method.signature - Full method signature in String form
## $method.reflectionCode - list of strings representing commented out reflection code to access method (Private Methods)
## $method.paramNames - List of Strings representing the method's parameters' names
## $method.paramClasses - List of Strings representing the method's parameters' classes
##
## You can configure the output class name using "testClass" variable below.
## Here are some examples:
## Test${entry.ClassName} - will produce TestSomeClass
## ${entry.className}Test - will produce SomeClassTest
##
########################################################################################
##
#macro (cap $strIn)$strIn.valueOf($strIn.charAt(0)).toUpperCase()$strIn.substring(1)#end
## Iterate through the list and generate testcase for every entry.
#foreach ($entry in $entryList)
#set( $testClass="${entry.className}Test")
##
package $entry.packageName;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.modules.junit4.PowerMockRunner;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* APPConstant Tester.
*
* @author shihaoyan
* @version 1.0
* @since ${today}
*/
@RunWith(PowerMockRunner.class)
public class $testClass {
private ${entry.className} entry = new ${entry.className}();
################################## 属性 #############################
## #foreach($field in $entry.fieldList)
## @Mock
## private $field.valueOf($field.charAt(0)).toUpperCase()$field.substring(1) $field;
## #end
###################controller层,如果需要注入request、respose进行创建#####
#set($a=0)
#foreach($method in $entry.methodList)
#foreach($classes in $method.paramClasses)
#if($classes.contains('HttpServletRequest')&&$a==0)
private MockHttpServletRequest request = new MockHttpServletRequest();
#set($a=1)
#end
#end
#end
#set($a=0)
#foreach($method in $entry.methodList)
#foreach($classes in $method.paramClasses)
#if($classes.contains('HttpServletResponse')&&$a==0)
private MockHttpServletResponse response = new MockHttpServletResponse();
#set($a=1)
#end
#end
#end
#################### end ###########################################
@Before
public void setUp() {
try {
${entry.className} entry1 = new ${entry.className}();
entry = PowerMockito.spy(entry);
entry.hashCode();
entry.equals(entry1);
####################################################################
#foreach($field in $entry.fieldList)
PowerMockito.field(${entry.className}.class,"$field").set(entry,$field);
EasyTestUtils.initMock($field);
#end
####################################################################
} catch (Exception e) {
}
}
#foreach($method in $entry.methodList)
#set($k=0)
#set($j=0)
#foreach($classes in $method.paramClasses)
#set($j=$j+1)
#end
/**
*
* Method: $method.signature
*
*/
@Test
public void test#cap(${method.name})() {
try {
entry.${method.name}(#foreach($classes in $method.paramClasses)#set($k=$k+1)#if($classes.contains('String')&&!$classes.contains('<')&&!$classes.contains('>')&&!$classes.contains('['))"abcd"#elseif($classes.contains('List'))null#elseif($classes.contains('HttpServletRequest'))request#elseif($classes.contains('HttpServletResponse'))response#elseif($classes.contains('long'))1000L#elseif($classes.contains('Date'))new Date()#elseif($classes.contains('int')&&!$classes.contains('['))1#elseif($classes.contains('int')&&$classes.contains('['))new int[]{1}#elseif($classes.contains('short'))(short)1#elseif($classes.contains('boolean'))true#elseif($classes.contains('byte[]'))"abcd".getBytes()#elseif($classes.contains('String')&&$classes.contains('['))new String[]{"abcd"}#elseif($classes.contains('byte')&&!$classes.contains('['))(byte)1#else null#end#if($k<$j),#end#end);
} catch (Exception e) {
}
}
#end
#foreach($method in $entry.privateMethodList)
#set($i=0)
#set($length=0)
#foreach($classes in $method.paramClasses)
#set($length=$length+1)
#end
#set($n=0)
/**
*
* Method: $method.signature
*
*/
@Test
public void test#cap(${method.name})() {
try {
PowerMockito.method(${entry.className}.class, "${method.name}"#foreach($classes in $method.paramClasses)#set($i=$i+1)#if($i==1),#end#if($classes.contains('String')&&!$classes.contains('<')&&!$classes.contains('>'))${classes.substring($classes.indexOf('String'))}.class#elseif($classes.contains('List'))List.class#elseif($classes.contains('Set'))Set.class#elseif($classes.contains('HttpServletRequest'))HttpServletRequest.class#elseif($classes.contains('HttpServletRespone'))HttpServletRespone.class#elseif($classes.contains('Map'))Map.class#else${classes}.class#end#if($i<$length),#end#end)
.invoke(entry #foreach($classes in $method.paramClasses)#set($n=$n+1)#if($n==1),#end#if($classes.contains('String')&&!$classes.contains('<')&&!$classes.contains('>'))"abcd"#elseif($classes.contains('List'))null#elseif($classes.contains('int')&&!$classes.contains('['))1#elseif($classes.contains('short'))(short)1#elseif($classes.contains('boolean'))true#elseif($classes.contains('byte[]'))"abcd".getBytes()#elseif($classes.contains('byte')&&!$classes.contains('[')) (byte)1 #else null #end#if($n<$length),#end#end);
} catch (Exception e) {
}
}
#end
}
#end
第二章 关于插件的使用
第一种:gen-test-plugin
这个是git提供的一个自动插件,能根据模板生成所有Test文件,但是灵活性比较差。推荐使用下面介绍的一款
第二种:JUnitGeneratorV2.0插件
- 安装方式:直接在idea设置里面安装就行了。
- 配置:
**注意:**Output Path: S O U R C E P A T H / . . / . . / t e s t / j a v a / {SOURCEPATH}/../../test/java/ SOURCEPATH/../../test/java/{PACKAGE}/${FILENAME}
这个一定改掉
-
模板配置
放在上面了。研究一下就能看懂,直接中就行。
-
使用方式:alt+insert
-
注意事项
- 如何使用了lomback 会出现错误,需要把源码中的lomback删掉然后手动生成getter/setter,在使用。
- 如果是service/controller这种需要注入服务对象的类需要把有一段代码开开,会自动生成Field并进行自动Mock
- 其他的直接用就行,但是会出现错误,但是错误不会很多,我们生成之后 简单调一下就行。
第三章 模板语言 Velocity 语法规则
- 定义变量: #set(i=1)
- 想要让变量+1:#set(i=i+1)
- 引用变量 $i
- 循环:#foreach($method in $entry.methodList) … #end
- 分支:#if() … #else() … #end
- 多分支:#if()… #elseif() … #end
- Java中的字符串处理函数都能,所以能玩很多东西。
- 学习网址:http://www.51gjie.com/javaweb/900.html
第四章 PowerMocker详解
1. 注解详解
- @RunWith(PowerMockRunner.class):每个测试类必须添加
- @PowerMockIgnore({“javax.management.*”}):不标记这个注解会出现一个红色的错误,虽然没有任何影响,但是真的非常丑。
- @PrepareForTest({}):这个注解非常重要,我们需要测试某些静态方法,或者私有方法,需要XXX.class写在上面。建议先把当前测试类 直接写进去。
- Mock:最重要的注解,我们当测试一个类的时候,肯定会出现调用其他的对象,比如说XxxService类,但是这个类可能就是一个接口,也可能太复杂了,不能直接使用,甚至有的还是spring注入的,那就没办法了。我们需要通过声明一个需要的对象,然后添加一个Mock注解,这样就不会出现空指针了,然后交给PowerMock进行管理。
2. 常用方法详解
- PowerMockito.when(xxxAAA.getId()).thenReturn(“123”); 这个方法是最重要的,先说明,当我们在一个方法中调用了另一个对象的方法,我们可以直接通过这个方法去模拟返回值,这个是必须的,因为我们调用任何方法都是为了得到一个结果。所以他是必须要会的。
- PowerMockito.doNothing().when(对象实例,“方法”,参数);这个可以模拟void方法
- Mockito.any()模拟一个任何参数
- Mockito.anyString()模拟String类型的参数
- Mockito.isNull()模拟null
- Mockito.anyXxx()什么都能模拟
- 注意:如果要模拟一个方法,出现一个模拟参数,那必须全部都用模拟参数,模拟的方法需要的参数需要注意是不是为null,需要一一对应的不然模拟不到。
- PowerMockito.mock(Xxxxx.class) 这个是模拟一个对象类似于@Mock
- PowerMockito.mockStatic(Xxxx.class) 如果需要模拟一个静态方法,需要先进行mockstatic,注意需要在@PrepareForTest中添加这个静态类。
- PowerMockito.spy(Xxx.class); 其实相当于我们能模拟这个实体类,也会走真正的方法。mock的对象不会走真正的方法的。
- PowerMockito.doReturn(返回值).when(对象实例, “方法”,参数列表); 这个方法表示不执行的模拟的方法体,就是调用这个方法 不走原先的方法体直接给你个值。
- PowerMockito.field(Xxxxxxxxx.class, “属性名称”).set(对象实例, 注入的值); 能够对方法中的某个属性进行注入。
3. 模拟各种方法
1. 模拟普通:PowerMockito.when(userInfo.getId()).thenReturn("123");
2. 模拟void方法:PowerMockito.doNothing().when(UserInfo.class,"setId","123"));
3. 模拟static方法(模拟静态方法)PowerMockito.when(UserInfo.getId()).thenReturn("123");
4. 模拟static void方法(模拟静态无返回值方法)和模拟void方法一样的。
5. 如果模拟一个普通方法,但是这个方法有异常,我们就需要不走真实的方法,直接走模拟:PowerMockito.doReturn(返回值).when(对象实例, "方法",参数列表);
6. 所有的模拟方法返回参数都可以设置多个,挺有意思的,因为这样可以根据第几次调用某个方法,得到不同的结果。
第五章 各种问题踩坑整理
PowerMocker和jacoco统计覆盖率为零的问题
这个问题其实很多是因为@PrepareForTest注解和jacoco出冲突了。因为jacoco是通过字节码插桩的方式进行统计的,所以有冲突,解决办法就是直接使用离线模式,就是前面介绍的方式。
模拟方法失效的问题
- 可能是因为参数不对,就是可能有的参数是null,这个需要单独模拟的。
- 可能模拟方式不对,需要变换一个模拟方式 比如所doReturn 或者 thenReturn啊
静态方法模拟失败
可能是没有在注解上声明@PrepareForTest
其他问题
如果碰到其他问题可以私信联系我,一般都能解决。