单元测试之JMockit,堪称无所不能
单元测试想必大家都知道,但我还是再说一遍,此段懂的同鞋可以跳过。
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证,在JAVA中就相当于一个类的方法,那是什么意思呢?就是这个方法在不受第三方、数据库等外界因素的前提下,能按照既定的结果正常执行,那么可以说此方法是OK的,但是呢要多验证多种case以确保此方法是百分百可行的。
那要怎么才排除外界因素?试想下,我们如果调用数据库的时候事先把返回结果先写好,那么是不是就可以省略调用了,直接用返回的结果,可是这不就改动源代码了吗,那肯定不行的,所以我们可以“模拟”返回,也就是mock,就是能让你的数据库调用的方法能正常执行,并且能返回你自己定义的结果。
现在有很多mock的框架,但是仅能mock一些public,non static or final的方法,在大多数情况下这并没有什么问题,他可以处理大多数的问题,但是当测试的代码包含了一些静态方法,可能就让问题变得难以解决,所以能胜任这个担当的只有JMockit,我对他只有四个字的感受,那就是无所不能,一不小心又点题了。
首先附上一个JMockit的中文网:http://jmockit.cn/showChannel.htm?channel=1,建议把它浏览一遍 理论是枯燥的,代码是有趣的,直接看代码,先来个Demo
import java.util.Locale;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import mockit.Expectations;
import mockit.integration.junit4.JMockit;
@RunWith(JMockit.class)
public class Demo {
@Test
public void testOne() {
Assert.assertTrue(Locale.getDefault().equals(Locale.CHINA));
Assert.assertTrue(Locale.getDefault().equals(Locale.ENGLISH));
}
}
上面是判断当前语言的,第一行和第二行代码肯定有一个执行失败。那么要怎么才能才能都成功呢,我把代码改一下
@Test
public void testOne() {
Assert.assertTrue(Locale.getDefault().equals(Locale.CHINA));
new Expectations(Locale.class) {
{
Locale.getDefault();
result = Locale.ENGLISH;
}
};
Assert.assertTrue(Locale.getDefault().equals(Locale.ENGLISH));
}
我在两者之间加了JMockit的一个API,让静态方法返回的结果是个固定值,这样我就可以实现不修改源码的前提下修改返回值了。这里有个双层花括号,意思是在Expectations加了个代码块,可以直接使用此对象的属性,result就是其中一个属性,代表返回值。
看完上完这个demo是不是觉得很容易,那现在来看个小案例吧
/**
* 数据库查询
*/
public interface DatabaseInvoke {
/**
* 根据用户姓名和账号查询该用户余额
* @param name
* @param account
* @return
*/
BigDecimal queryBalanceByNameAndAccount(String name,String account);
/**
* 存储过程调用,结果以修改入参的形式返回
* 判断客户是否允许查询
* 入参:clientId:{用户ID}
* 返参:allowVisit:{是否允许访问:Y-允许,N-禁止}
* @param map
*/
void clientAllowVisit(Map<String,Object> map);
}
/**
* 第三方查询
*/
public interface ThirdPartyInvoke {
/**
* 根据用户ID查询用户信息
* @param clientId
* @return
*/
MessageDto queryClientInfo(String clientId);
}
/**
* 业务代码
*/
public class Business{
@Resource
private DatabaseInvoke databaseInvoke;
@Resource
private ThirdPartyInvoke thirdPartyInvoke;
/**
* 判断此用户的余额是否大于指定阈值
* @param clientId
* @param threshold
* @return
*/
public boolean judgeBalanceMoreThanThreshold(String clientId,BigDecimal threshold) {
// 静态方法输出当前查询时间
System.out.println("查询时间:"+DateUtil.getNowTimeStr());
// 判空
if(clientId == null || threshold == null) {
return false;
}
// 判断该用户是否允许访问
Map<String,Object> map = new HashMap<String,Object>();
map.put("clientId", clientId);
databaseInvoke.clientAllowVisit(map);
String allowVisit = (String)map.get("allowVisit");
if(!("Y".equals(allowVisit))) {
return false;
}
// 私有方法将此操作记录数据库
this.recordOperation();
// 根据用户ID查询相关信息
MessageDto messageDto = thirdPartyInvoke.queryClientInfo(clientId);
// 用户姓名
String name = messageDto.getName();
// 用户账号
String account = messageDto.getAccount();
// 根据用户姓名和账号查询该用户余额
BigDecimal balance = databaseInvoke.queryBalanceByNameAndAccount(name, account);
if(balance!=null && balance.compareTo(threshold)>0) {
return true;
}
return false;
}
private void recordOperation() {
System.out.println("记录成功");
}
}
这里的案例涵盖了接口注入,接口调用的返回,入参的修改,那么这个单元测试要怎么写呢,API边看demo边讲
public class BusinessTest{
@Tested // 1
private Business business;
@Injectable // 2
private DatabaseInvoke databaseInvoke;
@Injectable
private ThirdPartyInvoke thirdPartyInvoke;
@Before
public void before() {
databaseInvoke = new MockUp<DatabaseInvoke>(DatabaseInvoke.class) {// 3
@Mock// 4
public void clientAllowVisit(Map<String,Object> map) {
map.put("allowVisit","Y");
};
@Mock
public BigDecimal queryBalanceByNameAndAccount(String name ,String account) {
if(name.equals("hzk") && account.equals("999")) {
return BigDecimal.valueOf(6000);
}else if(name.equals("jay") && account.equals("777")) {
return BigDecimal.valueOf(3000);
}else {
return null;
}
}
}.getMockInstance();
}
@Test
public void judgeBalanceMoreThanThresholdTest() {
Assert.assertTrue(!business.judgeBalanceMoreThanThreshold(null,BigDecimal.valueOf(4000)));
Assert.assertTrue(!business.judgeBalanceMoreThanThreshold("123",null));
Assert.assertTrue(!business.judgeBalanceMoreThanThreshold(null,null));
BigDecimal threshold = BigDecimal.valueOf(5000);
/*案例一*/
new Expectations(DateUtil.class) {// 5
{
String clientIdOne = "123456";
String nameOne = "hzk";
String accountOne = "999";
thirdPartyInvoke.queryClientInfo(clientIdOne);
MessageDto messageDto = new MessageDto();
messageDto.setName(nameOne);
messageDto.setAccount(accountOne);
result = messageDto;
DateUtil.getNowTimeStr();
result = "11111111";
}
};
new MockUp<Business>(Business.class) {// 6
@Mock
private void recordOperation() {
System.out.println("jilu shibai");
}
};
Assert.assertTrue(business.judgeBalanceMoreThanThreshold("123456", threshold));
/*案例二*/
new Expectations() {
{
String clientIdTwo = "654321";
String nameTwo = "jay";
String accountTwo = "777";
thirdPartyInvoke.queryClientInfo(clientIdTwo);
MessageDto messageDto = new MessageDto();
messageDto.setName(nameTwo);
messageDto.setAccount(accountTwo);
result = messageDto;
}
};
Assert.assertTrue(!business.judgeBalanceMoreThanThreshold("654321", threshold));
}
}
1.@Tested注解表示你要测试的类
2.加上了@Tested注解的类后要将其所有的注入属性全部加上@Injectable注解,包括它的父类,如果觉得麻烦那么可以继承它父类的单元测试,这样就不用当父类添加注入属性时还得改子类
3.重写一个注入属性的对象,要满足重写规则哦,不然没效果,这样子的好处是你可以在方法内控制既定的入参返回相应的结果,并且可以在其中修改入参,这是刚才的Expectations所不能比的
4.上面重写的方法要加上@Mock才能识别到
5.这里加了个入参,这代表此类将由JMockit实现,这样子你就可以在里面返回自己想要的结果,并且这个参数入口是个数组(Object…),你可以输入多个你想自定义的类
6.这里是对要测试的写进行重写,里面它包含了私有方法,在此处重写,请注意,new MockUp这个API要想对注入的属性进行重写在这里写是没用的,必须在属性初始值附上或者在@Before中实现,可以参考上面
最后说下注意事项:
- @RunWith(JMockit.class) 此注解可以不加,但有两个前提,满足之一就行了
① Junit5+版本以上
② JMockit依赖在Junit5-之前 - 运行时如果报错那么用JDK环境
- 如果有属性注入指定名字,例如Resource(name=”xxx”)时,那么用Injectable的注解时属性名要和name相同不然会找不到的,感谢吴国超大神的友情发现
- Tested注解指向的类必须要是你的实现类,如果指向接口那么会返回null的 5.mock当前对象时,即用this时记得在Expectations加上当前类的class对象哦,例如new Expectations(PersonService.Class){{}} 有问题欢迎留言讨论哈
- 如果有属性注入指定名字,例如Resource(name=”xxx”)时,那么用Injectable的注解时属性名要和name相同不然会找不到的