在介绍实际的例子之前,然我们先了解下什么是mock? 为什么mock?
mock的中文含义是假装模仿, 在单元测试里面我们测试的是某个单元的逻辑,即某个方法内的执行结果是否符合我们的预期。有一些方法会依赖于第三方的包,例如在service方法中我们有可能会去调用数据库的执行结果,会取redis中缓存数据,也有可能会使用当前的系统时间,根据系统时间做一些逻辑处理。虽然方法的逻辑依赖于第三方的东西,但是我们的单元测试却不能依赖于第三方的东西,你不可能用单元测试去测试数据库是不是可靠的,数据库的可靠性不是单元测试的目的, 这时候我们就要模仿数据库等第三方包的行为,让这些第三方包返回我们想要的东西,从而将依赖关系简单化,只测试我们自己的逻辑部分。
java的mock框架很多,最强大的是jmockit,本文就介绍jmockit的mock使用。
我们先介绍一下静态方法的mock,假定我们有这么一个方法需要测试:
import java.util.Calendar; import java.util.Date; /** * 任务服务 * Created by outofmemory.cn on 2015/10/28. */ public class TaskService { private static final int YESTERDAY_TASK_LIMIT_HOUR = 8; /** * 根据任务创建的时间判断任务是否可以执行 * * 如果任务是当天创建的允许执行, 如果是昨天的任务,允许在第二天的早上YESTERDAY_TASK_LIMIT_HOUR点前执行 * @param taskCreateTime 任务创建时间 * @return true 任务可以执行, false 任务已过期不可以执行 */ public boolean canExecute(Date taskCreateTime) { Date now = new Date(); Calendar nowCalendar = Calendar.getInstance(); nowCalendar.setTime(now); Calendar createTimeCalendar = Calendar.getInstance(); createTimeCalendar.setTime(taskCreateTime); boolean isToday = isSameDay(taskCreateTime, now); //如果是当天任务允许执行 if (isToday) { return true; } nowCalendar.add(Calendar.DATE, -1); Date yesterday = nowCalendar.getTime(); boolean isYesterday = isSameDay(taskCreateTime, yesterday); //如果是昨天任务,只允许在8点前执行 if (isYesterday) { return nowCalendar.get(Calendar.HOUR_OF_DAY) < YESTERDAY_TASK_LIMIT_HOUR; } return false; } /** * 判断两个日期是否为同一天 * @param date1 date1 * @param date2 date2 * @return 是同一天返回true, 否则为false */ private boolean isSameDay(Date date1, Date date2) { Calendar calendar1 = Calendar.getInstance(); calendar1.setTime(date1); Calendar calendar2 = Calendar.getInstance(); calendar2.setTime(date2); return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR) && calendar1.get(Calendar.MONTH) == calendar2.get(Calendar.MONTH) && calendar1.get(Calendar.DAY_OF_MONTH) == calendar2.get(Calendar.DAY_OF_MONTH); } }
canExecute方法的逻辑依赖于当前的时间, 当任务的创建时间和now是同一天时,任务可以执行,当任务的创建时间是昨天时, 需要在今天的8点之前执行。 这个方法的测试也需要依赖于当前时间,所以我们要mock当前时间。
java中当前时间是根据System.currentTimeMillis()的返回值设定的, 要mock当前的时间需要mock System类的这个方法。具体的mock操作请看代码,请注意注释:
import mockit.Expectations; import mockit.Mocked; import mockit.integration.junit4.JMockit; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import java.util.Calendar; import java.util.Date; /** * TaskService 测试类, 注意此处需要添加@RunWith(JMockit.class), 否则jmockit无法做mock操作 * * Created by outofmemory.cn on 2015/10/28. */ @RunWith(JMockit.class) public class TaskServiceTest { /** * 要mock当前时间需要mock System类,这里声明一个System类型的字段,并添加@Mocked注解,表示要对此类做mock */ @Mocked @SuppressWarnings({"UnusedDeclaration"}) private System system; @Test public void canExecuteTest() { final Calendar cal = Calendar.getInstance(); cal.set(2015, Calendar.NOVEMBER, 28, 10, 10, 0); //mock new Expectations(){{ //指定要mock的方法 System.currentTimeMillis(); //指定mock方法要返回的结果 result = cal.getTime().getTime(); }}; cal.set(2015, Calendar.NOVEMBER, 28, 15, 0, 0); Date createTime = cal.getTime(); //执行方法逻辑 TaskService taskService = new TaskService(); boolean canExecute = taskService.canExecute(createTime); //验证是否符合预期 Assert.assertTrue(canExecute); } }
上述例子是对System类方法的mock,下面我们看一个更接近实际的例子,我们有一个Service类,该Service类引用了另外一个Service, 我们要mock被引用service的方法。
被测试的类如下, ServiceUseRedis,他引用了RedisService。
/** * ServiceUseRedis 被测试类,该类引用了redisService, redisService在测试中将被mock * * Created by outofmemory.cn on 2015/10/26. */ public class ServiceUseRedis { private RedisService redisService; public boolean doSomethingReturnBoolean(String someArg) { String somethingInRedis = redisService.get(someArg); return someArg.equals(somethingInRedis); } }
RedisService类如下:
/** * Created by outofmmeory.cn on 2015/10/26. */ public class RedisService { public String get(String key) { throw new RuntimeException("I'm redis service, you can not call me in unit test"); } }
处于测试目的,这个类只有一个方法get(String),方法直接抛出一个运行时异常, 而我们在测试时会mock此方法,让此方法返回我们需要的结果。
测试类如下:
import mockit.Expectations; import mockit.Injectable; import mockit.Mocked; import mockit.Tested; import mockit.integration.junit4.JMockit; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; /** * 测试一个类引用另外一个类, 被引用类的方法要做mock * * Created by outofmemory.cn on 2015/10/26. */ @RunWith(JMockit.class) public class ServiceUseRedisTest { /** * 被引用类, 被mock */ @Mocked @Injectable RedisService redisService; /** * 要测试的类 */ @Tested ServiceUseRedis serviceTested; @Test public void doSomethingReturnBooleanTest() { // 设置mock期望 new Expectations(){{ redisService.get("abc"); result = "abc"; }}; //执行service方法 boolean doResult = serviceTested.doSomethingReturnBoolean("abc"); //验证执行结果 Assert.assertTrue(doResult); } }
我们可以运行测试,测试通过,说明redisService的get方法已经成功的被我们mock掉,返回了我们想要的abc,而不是抛出运行时异常。
最后提供下jmockit需要的maven依赖:
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jmockit</groupId> <artifactId>jmockit</artifactId> <version>1.20</version> <scope>test</scope> </dependency>
jmockit是一个非常优秀的mock框架,可以很方便的帮我们隔离单元测试的依赖逻辑。 单元测试的核心思想是单元,每一个单元测试都只测试自己关注的逻辑,mock掉依赖才可以更好的测试单元逻辑。
原文链接:http://outofmemory.cn/java/jmockit-mock-anything