单元测试之JMockit,堪称无所不能

单元测试之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中实现,可以参考上面
最后说下注意事项:

  1. @RunWith(JMockit.class) 此注解可以不加,但有两个前提,满足之一就行了
    ① Junit5+版本以上
    ② JMockit依赖在Junit5-之前
  2. 运行时如果报错那么用JDK环境
  3. 如果有属性注入指定名字,例如Resource(name=”xxx”)时,那么用Injectable的注解时属性名要和name相同不然会找不到的,感谢吴国超大神的友情发现
  4. Tested注解指向的类必须要是你的实现类,如果指向接口那么会返回null的 5.mock当前对象时,即用this时记得在Expectations加上当前类的class对象哦,例如new Expectations(PersonService.Class){{}} 有问题欢迎留言讨论哈
  5. 如果有属性注入指定名字,例如Resource(name=”xxx”)时,那么用Injectable的注解时属性名要和name相同不然会找不到的
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值