为了测试与时间相关的代码,最好使用虚拟时钟模式
我们的想法是,我们不使用new Date
提取当前时间,而是从一个时钟中提取,该时钟可以用返回预定义固定时间的虚拟时钟来模拟。
现在在Java中,我们有带有DateTime
类的JodaTime,它允许使用设置采样时间
DateTimeUtils.setCurrentMillisFixed(today.getMillis());
并使用将固定时间重置为系统时间
DateTimeUtils.setCurrentMillisSystem();
这里有一篇关于如何将它与TestNG一起使用的好文章。
现在问题来了!
如果在全局上下文中为运行测试的时间全局设置固定时间,那么将此技术与setUp和tearDown方法一起使用是多么安全。只要我得到了它——它只会在我们没有两个并行测试的情况下工作,而这项技术在同一个环境中并行运行。
必须确保在tearDown
方法中调用DateTimeUtils.setCurrentMillisSystem()
。这样一个测试就不会影响另一个测试。即使测试中出现异常,TestNG也应该调用tearDown
。
当我想将类与System.currentTimeMillis();
解耦时,我通常更喜欢另一种方式。我介绍了一个接口Clock
和一个类似的实现SystemClock
:
public interface Clock {
public long getCurrentTimeMillis();
}
public class SystemClock implements Clock {
public long getCurrentTimeMillis(){
return System.currentTimeMillis();
}
}
对于测试,可以很容易地创建一个mock,该mock在每次调用时返回一个固定的时间或一系列预定义的时间。
有些人可能会认为,引入这样一个接口来只解耦一个方法是过度设计的,这会对性能产生影响。但幸运的是,我们有一个JIT编译器,因为JIT知道只加载了SystemClock
类,所以它知道(目前)不存在其他实现。在这个假设下,它可以只使用内联方法。
所以我更喜欢用测试最好的方式编写代码。
编辑
对于Java 8,您可能希望使用Supplier<Long>
接口。
例如,在您的客户端代码中,您可以使用方法引用
public class SomeClass {
private Supplier<Long> currentTimeMillisSupplier;
public SomeClass(){
this(System::currentTimeMillis);
}
SomeClass(Supplier<Long> currentTimeMillisSupplier){
this.currentTimeMillisSupplier = currentTimeMillisSupplier;
}
}
默认构造函数用于"正常"使用,而其他包范围的构造函数可用于单元测试。只需将测试类放在同一个包中即可。
您也可以使用Clock
接口,因为它是@FunctionalInterface
。
public class SomeClass {
private Clock clock;
public SomeClass(){
this(System::currentTimeMillis);
}
public SomeClass(Clock clock){
this.clock = clock;
}
}