2019年8月27日 随笔
JMockit学习
概述
JMockit是一款Java类/接口/对象的Mock工具,目前广泛应用于Java应用程序的单元测试中。
配置
Maven porm.xml
<!-- 先声明jmockit的依赖 -->
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.36</version>
<scope>test</scope>
</dependency>
<!-- 再声明junit的依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
<scope>test</scope>
</dependency>
JUnit4.x及以下注意,如果你是通过mvn test来运行你的测试程序 , 请确保JMockit的依赖定义出现在JUnit的依赖之前。
JMockit Coverage配置
JMockit的代码覆盖率功能
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:"${settings.localRepository}/org/jmockit/jmockit/1.36/jmockit-1.36.jar=coverage"</argLine>
<disableXmlReport>false</disableXmlReport>
<systemPropertyVariables>
<coverage-output>html</coverage-output>
<coverage-outputDir>${project.build.directory}/codecoverage-output</coverage-outputDir>
<coverage-metrics>all</coverage-metrics>
</systemPropertyVariables>
</configuration>
</plugin>
JMockit的程序结构
//JMockit的程序结构
public class ProgramConstructureTest {
// 这是一个测试属性
@Mocked
HelloJMockit helloJMockit;
@Test
public void test1() {
// 录制(Record)
new Expectations() {
{
helloJMockit.sayHello();
// 期待上述调用的返回是"hello,david",而不是返回"hello,JMockit"
result = "hello,david";
}
};
// 重放(Replay)
String msg = helloJMockit.sayHello();
Assert.assertTrue(msg.equals("hello,david"));
// 验证(Verification)
new Verifications() {
{
helloJMockit.sayHello();
times = 1;
}
};
}
@Test
public void test2(@Mocked HelloJMockit helloJMockit /* 这是一个测试参数 */) {
...
}
}
测试属性&测试参数
测试属性
测试类的一个属性。它用作与测试类的所有方法。
在JMockit中,用@Mocked修饰了测试属性HelloJMockit helloJMockit,表示helloJMockit这个测试属性,它的实例化,属性赋值,方法调用的返回值全部由JMockit来接管,接管后,helloJMockit的行为与HelloJMockit类定义的不一样了,而是由录制脚本来定义了。
测试参数
测试方法的参数,它仅仅用作与当前测试方法。测试参数和测试属性的不同是在作用域上。
Record-Replay-Verification
- Record-Replay-Verification:
是JMockit测试程序的主要结构。 - Record:
即先录制某类/对象的某个方法调用,在当输入什么时,返回什么。 - Replay:
即重放测试逻辑。 - Verification:
重放后的验证。比如验证某个方法有没有被调用,调用多少次。
@Mocked
功能
@Mocked修饰的类/接口,是告诉JMockit,帮我生成一个Mocked对象,这个对象方法(包含静态方法)返回默认值。
即如果返回类型为原始类型(short,int,float,double,long)就返回0,
如果返回类型为String就返回null;
如果返回类型是其它引用类型,则返回这个引用类型的Mocked对象;
什么时候使用@Mocked
当我们的测试程序依赖某个接口时,只需要@Mocked一个注解,JMockit就能帮我们生成这个接口的实例。
比如在分布式系统中,我们的测试程序依赖某个接口的实例是在远程服务器端时,我们在本地构建是非常困难的,此时就交给
@Mocked。
@Injectable 和 @Mocked
区别
@Injectable 也是告诉 JMockit生成一个Mocked对象,但@Injectable只是针对其修饰的实例,而@Mocked是针对其修饰类的所有实例。
此外,@Injectable对类的静态方法,构造函数没有影响。因为它只影响某一个实例。
使用总结
@Injectable 也表示一个Mocked对象,相比@Mocked,只不过只影响类的一个实例;
@Mocked默认是影响类的所有实例;
@Tested表示被测试对象。如果该对象没有赋值,JMockit会去实例化它;
若@Tested的构造函数有参数,则JMockit通过在测试属性&测试参数中查找@Injectable修饰的Mocked对象注入@Tested对象的构造函数来实例化,不然,则用无参构造函数来实例化。除了构造函数的注入,JMockit还会通过属性查找的方式,把@Injectable对象注入到@Tested对象中。
注入的匹配规则:先类型,再名称(构造函数参数名,类的属性名)。若找到多个可以注入的@Injectable,则选择最优先定义的@Injectable对象。
使用场景
当我们需要手工管理被测试类的依赖时,就需要用到@Tested & @Injectable。两者搭配起来用,JMockit就能帮我们轻松搞定被测试类及其依赖注入细节。
@Capturing
@Capturing主要用于子类/实现类的Mock
使用场景
当我们只知道父类或接口时,但我们需要控制它所有子类的行为时,子类可能有多个实现(可能有人工写的,也可能是AOP代理自动生成时)。就用@Capturing。
Expectations
Expectations的作用主要是用于录制。即录制类/对象的调用。
录制脚本规范
new Expectations() {
// 这是一个Expectations匿名内部类
{
// 这是这个内部类的初始化代码块,我们在这里写录制脚本,脚本的格式要遵循下面的约定
//方法调用(可是类的静态方法调用,也可以是对象的非静态方法调用)
//result赋值要紧跟在方法调用后面
//...其它准备录制脚本的代码
//方法调用
//result赋值
}
};
还可以再写new一个Expectations,只要出现在重放阶段之前均有效。
new Expectations() {
{
//...录制脚本
}
};
录制的两种方式
1.通过引用外部类的Mock对象(@Injectabe,@Mocked,@Capturing)来录制
//Expectations对外部类的mock对象进行录制
public class ExpectationsTest {
@Mocked
Calendar cal;
@Test
public void testRecordOutside() {
new Expectations() {
{
// 对cal.get方法进行录制,并匹配参数 Calendar.YEAR
cal.get(Calendar.YEAR);
result = 2016;// 年份不再返回当前小时。而是返回2016年
// 对cal.get方法进行录制,并匹配参数 Calendar.HOUR_OF_DAY
cal.get(Calendar.HOUR_OF_DAY);
result = 7;// 小时不再返回当前小时。而是返回早上7点钟
}
};
Assert.assertTrue(cal.get(Calendar.YEAR) == 2016);
Assert.assertTrue(cal.get(Calendar.HOUR_OF_DAY) == 7);
// 因为没有录制过,所以这里月份返回默认值 0
Assert.assertTrue(cal.get(Calendar.DAY_OF_MONTH) == 0);
}
}
在Expectations匿名内部类的初始代码块中,我们可以对外部类的任意成员变量,方法进行调用。大大便利我们书写录制脚本。
2.通过构建函数注入类/对象来录制
有时候,我们只希望JMockit只mock类/对象的某一个方法。
//通过Expectations对其构造函数mock对象进行录制
public class ExpectationsConstructorTest2 {
// 把类传入Expectations的构造函数
@Test
public void testRecordConstrutctor1() {
Calendar cal = Calendar.getInstance();
// 把待Mock的类传入Expectations的构造函数,可以达到只mock类的部分行为的目的
new Expectations(Calendar.class) {
{
// 只对get方法并且参数为Calendar.HOUR_OF_DAY进行录制
cal.get(Calendar.HOUR_OF_DAY);
result = 7;// 小时永远返回早上7点钟
}
};
Calendar now = Calendar.getInstance();
// 因为下面的调用mock过了,小时永远返回7点钟了
Assert.assertTrue(now.get(Calendar.HOUR_OF_DAY) == 7);
// 因为下面的调用没有mock过,所以方法的行为不受mock影响,
Assert.assertTrue(now.get(Calendar.DAY_OF_MONTH) == (new Date()).getDate());
}
// 把对象传入Expectations的构造函数
@Test
public void testRecordConstrutctor2() {
Calendar cal = Calendar.getInstance();
// 把待Mock的对象传入Expectations的构造函数,可以达到只mock类的部分行为的目的,但只对这个对象影响
new Expectations(cal) {
{
// 只对get方法并且参数为Calendar.HOUR_OF_DAY进行录制
cal.get(Calendar.HOUR_OF_DAY);
result = 7;// 小时永远返回早上7点钟
}
};
// 因为下面的调用mock过了,小时永远返回7点钟了
Assert.assertTrue(cal.get(Calendar.HOUR_OF_DAY) == 7);
// 因为下面的调用没有mock过,所以方法的行为不受mock影响,
Assert.assertTrue(cal.get(Calendar.DAY_OF_MONTH) == (new Date()).getDate());
// now是另一个对象,上面录制只对cal对象的影响,所以now的方法行为没有任何变化
Calendar now = Calendar.getInstance();
// 不受mock影响
Assert.assertTrue(now.get(Calendar.HOUR_OF_DAY) == (new Date()).getHours());
// 不受mock影响
Assert.assertTrue(now.get(Calendar.DAY_OF_MONTH) == (new Date()).getDate());
}
}
MockUp & @Mock
它的Mock方式最直接。
MockUp & @Mock比较适合于一个项目中,用于对一些通用类的Mock,以减少大量重复的new Exceptations{{}}代码。
//Mockup & @Mock的Mock方式
public class MockUpTest {
@Test
public void testMockUp() {
// 对Java自带类Calendar的get方法进行定制
// 只需要把Calendar类传入MockUp类的构造函数即可
new MockUp<Calendar>(Calendar.class) {
// 想Mock哪个方法,就给哪个方法加上@Mock, 没有@Mock的方法,不受影响
@Mock
public int get(int unit) {
if (unit == Calendar.YEAR) {
return 2017;
}
if (unit == Calendar.MONDAY) {
return 12;
}
if (unit == Calendar.DAY_OF_MONTH) {
return 25;
}
if (unit == Calendar.HOUR_OF_DAY) {
return 7;
}
return 0;
}
};
// 从此Calendar的get方法,就沿用你定制过的逻辑,而不是它原先的逻辑。
Calendar cal = Calendar.getInstance(Locale.FRANCE);
Assert.assertTrue(cal.get(Calendar.YEAR) == 2017);
Assert.assertTrue(cal.get(Calendar.MONDAY) == 12);
Assert.assertTrue(cal.get(Calendar.DAY_OF_MONTH) == 25);
Assert.assertTrue(cal.get(Calendar.HOUR_OF_DAY) == 7);
// Calendar的其它方法,不受影响
Assert.assertTrue((cal.getFirstDayOfWeek() == Calendar.MONDAY));
}
}
弊端
- 一个类有多个实例。只对其中某1个实例进行mock。
最新版的JMockit已经让MockUp不再支持对实例的Mock了。1.19之前的老版本仍支持。 - AOP动态生成类的Mock。
- 对类的所有方法都需要Mock时,书写MockUp的代码量太大。如web程序中,经常需要对HttpSession进行Mock。若用MockUp你要写大量的代码,可是用@Mocked就一行代码就可以搞定。
Verifications
Verifications是用于做验证。验证Mock对象(即@Moked/@Injectable@Capturing修饰的或传入Expectation构造函数的对象)有没有调用过某方法,调用了多少次。与Exceptations的写法相似。
new Verifications() {
// 这是一个Verifications匿名内部类
{
// 这个是内部类的初始化代码块,我们在这里写验证脚本,脚本的格式要遵循下面的约定
//方法调用(可是类的静态方法调用,也可以是对象的非静态方法调用)
//times/minTimes/maxTimes 表示调用次数的限定要求。赋值要紧跟在方法调用后面,也可以不写(表示只要调用过就行,不限次数)
//...其它准备验证脚本的代码
//方法调用
//times/minTimes/maxTimes 赋值
}
};
//还可以再写new一个Verifications,只要出现在重放阶段之后均有效。
new Verifications() {
{
//...验证脚本
}
};
通常,在实际测试程序中,我们更倾向于通过JUnit/TestNG/SpringTest的Assert类对测试结果的验证, 对类的某个方法有没调用,调用多少次的测试场景并不是太多。因此在验证阶段,我们完全可以用JUnit/TestNG/SpringTest的Assert类取代new Verifications() {{}}验证代码块。
网友评论