记一次关于mock Systemc.currentTimeMillis的实践
因为在写单测过程中,发现@PrepareForTest和JaCoCo会有冲突,所以想要将JaCoCo修改为offline模式,但是这样一来,就需要对utils等模块全部重新写单测。
从单测的角度来说各个模块的单测各自独立是比较推荐的,模块A的单测就写在模块A里。但是很多工具类单独写单测也还是比较麻烦的,就想着是否有其他办法可以绕过这个问题
介于我们项目中需要使用@PrepareForTest的场景几乎都是用于mock系统时间戳(其他场景下用于第三方类并不影响我们项目覆盖率的统计),由于使用@PrepareForTest会导致调用了系统时间戳的类无法统计覆盖率,所以目前项目中有很多涉及到时间的数据构造都是通过采用在当前时间戳的基础上加减小时数来做到的。这样一来就会有几个问题
- 其实这样单测的可读性不好
- 对于一些涉及到时间的断言在写的时候也是通过调用函数获取预期值,和单测的目的也相违背
就想到针对我们项目的这种情形,是否可以不使用JaCoCo的离线模式,而采用其他办法在测试类中mock系统时间戳
封装工具类,替换掉Systemc.currentTimeMillis()
最简单的办法就是将System.currentTimeMillis()封装成一个静态工具类,把所有调用系统时间戳的地方都改成调用该方法,这样在使用PowerMock的时候就不用担心系统时间戳的调用类无法统计到覆盖率
public class SystemUtils {public static Long currentTimeMillis(){return System.currentTimeMillis();}
}
//原来的public static boolean isBeforeTime(long beginTimeUTC, long offset) {long timeNow = System.currentTimeMillis();if (beginTimeUTC - timeNow > THREE_DAYS) {return true;} return false;}//修改后public static boolean isBeforeTime(long beginTimeUTC, long offset) {long timeNow = SystemUtils.currentTimeMillis();if (beginTimeUTC - timeNow > THREE_DAYS) {return true;} return false;}
使用其他时间戳api
java 8 提供了新的关于日期操作的api, 相比以前的api更加易用和安全,推荐使用java 8的api替换上述获取系统时间戳的方法。 比方说下面的LocalDateTime,我们可以通过 Clock.fixed在写单测的时候将系统时间设置成任何你想要的值,或者直接mockLocalDateTime.now返回的时间结果也是可以的
private static Clock clock = Clock.systemDefaultZone();private static ZoneId zoneId = ZoneId.systemDefault();public static Long getSystemTime(){return LocalDateTime.now(clock).atZone(zoneId).toInstant().toEpochMilli();}//设置当前系统时间为指定的LocalDateTimepublic static void useFixedClockAt(LocalDateTime date) {clock = Clock.fixed(date.atZone(zoneId).toInstant(), zoneId);}//恢复当前系统时间public static void useSystemDefaultZoneClock() {clock = Clock.systemDefaultZone();}
使用AspectJ代理
我只是想要将系统时间戳的方法调用结果进行修改,那么是否可以使用代理来实现呢?
根据参考资料,我先尝试了使用AspectJ来实现对Systemc.currentTimeMillis()方法的代理
public aspect ChangeSystemTimeMillis {long around(): call(* java.lang.System.currentTimeMillis()){return TimeMachine.getSystemTimestamp();}
}
public class TimeMachine {public static Long systemTimestamp = 0L ;public static Long getSystemTimestamp() {return systemTimestamp;}public static void setSystemTimestamp(String datetime) throws ParseException {SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Date date = simpleDateFormat.parse(datetime);TimeMachine.systemTimestamp = date.getTime();}public static void setSystemTimestamp(Long systemTimestamp) {TimeMachine.systemTimestamp = systemTimestamp;}}
这样,我们就可以通过setSystemTimestamp方法设置系统时间戳的返回值,默认返回0
但是在验证过程中发现一个问题:
我只是想要在测试代码里对系统时间戳的调用进行代理,在测试或者生产环境真正调用代码的时候不需要这个代理操作,在网上查了下资料发现可以通过aspectj-maven-plugin插件实现 , 配置如下
<plugin><groupId>org.codehaus.mojo</groupId><artifactId>aspectj-maven-plugin</artifactId><version>1.10</version><configuration><showWeaveInfo>true</showWeaveInfo><source>1.8</source><complianceLevel>1.8</complianceLevel><target>1.8</target></configuration><executions><execution><!--该织入在test-compile阶段执行,需要先通过maven执行该过程才可以得到代理class--><id>test-compile</id><configuration><!--在执行测试编译的时候会对源代码做织入生成新的class文件--><weaveMainSourceFolder>true</weaveMainSourceFolder></configuration><goals><goal>test-compile</goal></goals></execution></executions></plugin>
这样做,在执行mvn相关命令的是(test-compile,package),可以在test-classes下看到生成的被织入的class文件
(但是没有事先通过maven编译是无法生成对应的代理class,也就无法起到修改系统时间戳的目的)
织入成功后的class
private boolean isOverdue(Long beginTime, Double timeZone) {return (double)(beginTime + 28800000L) - timeZone * 3600.0D * 1000.0D + (double)OVERDUE_TIME < (double)currentTimeMillis_aroundBody1$advice(this, ChangeSystemTimeMillis.aspectOf(), (AroundClosure)null);}
本来以为这样就成功了,但是在提交执行单测的时候又发现了另一个问题
由于JaCoCo需要通过修改字节码文件加入统计代码类统计覆盖率,而上述aspectj-maven-plugin在编译时在test-classes下生成了新的class文件,这里是不包含统计代码的,所以哪怕我的单测都通过了,原来的代码也没有被JaCoCo统计到覆盖率
这就跟一开始的目的相违背了,所以最后还是采用了离线JaCoCo模式来解决这个问题。
记录一下这次探索,我看到StackOverFlow有人建议使用LTW,不过我没尝试成功,那个对系统类好像并不有效,如果有人解决了这个问题欢迎一起分享下
参考资料:
Overriding System Time for Testing in Java
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
