时间相关的单元测试


75

我需要测试一个函数,该函数的结果将取决于当前时间(使用Joda time的时间isBeforeNow())。

public boolean isAvailable() {
    return (this.someDate.isBeforeNow());
}

是否可以使用(例如使用Mockito)存根/模拟系统时间,以便我可以可靠地测试功能?


2
对于某些功能,最简单的解决方案是将当前时间作为参数传递。
DariusL

直到您由于日光节约突然导致单元测试失败;)
pojo 2015年

1
就像模拟一样,它不必是实际的当前时间。您可以对安全时间瞬间进行硬编码。
DariusL

Answers:


64

Joda time支持通过类的setCurrentMillisFixedsetCurrentMillisOffset方法设置“假”当前时间DateTimeUtils

参见https://www.joda.org/joda-time/apidocs/org/joda/time/DateTimeUtils.html


11
但这是静态方法-您将以这种方式介绍单元测试之间的依赖关系。因此,我更喜欢Jon Skeets解决方案。
汉斯·彼得·斯托尔2012年

hstoerr:我看不到测试之间会有依赖关系,除非它们要在不同的线程中执行(这里可能不是这种情况)。但是即使那样,Joda Time仍然提供了该DateTimeUtils.setCurrentMillisProvider(DateTimeUtils.MillisProvider)方法,该方法肯定会允许线程绑定的实现。
罗杰里奥(Rogério)2012年

128

使代码可测试的最佳方法(IMO)是将“当前时间是什么”的依赖项提取到其自己的界面中,该实现使用当前系统时间(通常使用),而实现则可以设置时间,根据需要进行升级等。

我在各种情况下都使用了这种方法,并且效果很好。设置很容易-只需创建一个界面(例如Clock),该界面即可通过一种方法以所需的任意格式(例如,使用Joda Time或可能使用Date)为您提供当前时刻。


7
Joda time已内置了对该抽象的支持(请参阅我的答案),因此您不必在代码中引入它。
Laurent Pireyn 2011年

21
@Laurent:我认为这实际上没有那么优雅。从根本上讲,我认为诸如“获取当前时间”之类的服务依赖项(就像我将随机数生成视为依赖项一样),因此我认为最好将其明确化。这意味着您也可以并行化测试等。
乔恩·斯基特

5
点了。不要误会我的意思:我通常赞成抽象。但是,在现实生活中的项目中可能很难抽象出当前时间的概念,尤其是当使用了不抽象该概念的第三方库时。
Laurent Pireyn 2011年

3
@Laurent:哦,是的,当您使用第三方库时,这很棘手……但是除非您的第三方库碰巧使用了Joda Time,否则setCurrentMillisFixed会遇到相同的问题:(
Jon Skeet

3
@乔恩:我完全同意你的看法。现在,Java 8具有抽象类Clock。
xli

22

Java 8引入了抽象类java.time.Clock,该类允许您使用替代实现进行测试。这正是乔恩当时在回答中所暗示的。


1
如果您所需要的只是询问“当前时间是几点?”的能力,则您不需要整个时钟。您可能更喜欢使用Supplier感兴趣的时间单位(例如LocalDateTime)。
jub0bs

1
@Jubobs如果LocalDateTime.now()不将Clock传递为,则无法控制当前返回的时间now()
迪蒙

1
@deamon我的建议是有一个type端口Supplier<LocalDateTime>,您可以在其中方便(单元测试)的地方插入真实的适配器(例如LocalDateTime::now-)或伪造的适配器(例如() -> LocalDateTime.of(2017, 10, 24, 0, 0))。不需要整体Clock
jub0bs 17-10-24

4

要添加到Jon Skeet的答案中,Joda Time已经包含当前时间界面: DateTimeUtils.MillisProvider

例如:

import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils.MillisProvider;

public class Check {
    private final MillisProvider millisProvider;
    private final DateTime someDate;

    public Check(MillisProvider millisProvider, DateTime someDate) {
        this.millisProvider = millisProvider;
        this.someDate = someDate;
    }

    public boolean isAvailable() {
        long now = millisProvider.getMillis();
        return (someDate.isBefore(now));
    }
}

在单元测试中模拟时间(使用Mockito,但您可以实现自己的类MillisProviderMock):

DateTime fakeNow = new DateTime(2016, DateTimeConstants.MARCH, 28, 9, 10);
MillisProvider mockMillisProvider = mock(MillisProvider.class);
when(mockMillisProvider.getMillis()).thenReturn(fakeNow.getMillis());

Check check = new Check(mockMillisProvider, someDate);

使用生产中的当前时间(在2.9.3中将DateTimeUtils.SYSTEM_MILLIS_PROVIDER添加到Joda Time中):

Check check = new Check(DateTimeUtils.SYSTEM_MILLIS_PROVIDER, someDate);

1

我使用的方法类似于Jon的方法,但Clock我通常不创建一个专门的界面(例如),而是创建一个特殊的测试界面(例如MockupFactory)。我把测试代码所需的所有方法放在这里。例如,在我的一个项目中,我有四种方法:

  • 一个返回模拟数据库客户端的客户端;
  • 一个创建模拟通知器对象的对象,该对象通知代码有关数据库中的更改;
  • 一个创建模型java.util.Timer的模型,该模型在需要时运行任务;
  • 返回当前时间的一个。

被测试的类具有一个构造函数,该构造函数在其他参数中接受此接口。没有这个参数的人只是创建该接口的默认实例,该实例在“现实生活中”起作用。接口和构造函数都是包私有的,因此测试API不会泄漏到包外部。

如果我需要更多的模仿对象,则只需向该接口添加一个方法,然后在测试和实际实现中都将其实现。

这样一来,我设计了适合于测试的代码,而又没有对代码本身施加太多的负担。实际上,由于许多工厂代码集中在一个地方,因此代码变得更加简洁。例如,如果我需要切换到实际代码中的另一个数据库客户端实现,则只需要修改一行,而不必四处寻找对构造函数的引用。

当然,就像乔恩(Jon)的方法一样,它不适用于您无法修改或不允许修改的第三方代码。


听起来您要创建一个新接口来封装每个被测类的依赖项?这将需要至少一种实现。因此,现在对于每个要测试的类,您都有该类,至少一个测试类,依赖项分组接口以及该接口的实现?我真的不喜欢 而且,我想在代码及其实际依赖关系之间添加额外的间接级别(即,在查看了您的类之后,我还必须查看依赖关系接口)实际上使代码更难理解。
丹丹

@Dathan,不,不是每个班级。仅那些具有必须在测试期间进行仿真的依赖项的程序。在我的应用程序中,碰巧只有一个这样的类。同样,类的数量也没有任何意义。如果只是实现class DefaultMockupFactory implements MockupFactory {Timer createTimer() {return new Timer();}}而已,那就不是那么复杂了吗?而且factory.createTimer()在代码中的某处也不会使代码更难以理解。但我同意,在某些情况下,这可能不是最佳方法。
Sergei Tachenov 2013年

不,我不认为这会增加过多的复杂性-只是不必要的复杂性。我感觉通过拥有此Facade接口而增加的间接调用级别可能会抑制代码的可读性。例如,如果我有您的代码示例,但是上面MockupFactory有其他一些方法,并且我想查找代码中createTimer()使用过的所有位置,则必须从被测类导航到接口,然后搜索用于方法的使用,而不仅仅是在类中搜索。
丹丹

@Dathan,该接口是嵌套的(具有程序包专用可见性),因此它仍然在类中。而且无论如何,我和Jon的方法之间的区别在于,我MockupFactory对必须模拟的所有依赖项都使用一个接口(),而Jon建议为每个依赖项(TimerFactory等等)使用单独的接口。在没有任何接口的情况下如何测试代码对我来说是一个谜。一种或另一种方式,需要额外的复杂性。
谢尔盖·塔切诺夫

1
我同意,需要添加一些接口。但是我宁愿遵循接口隔离-因此在这种情况下,我强烈希望使用Jon的方法。部分原因是许多接口在系统中被重用-很有可能TimerFactory会被重用,因此您可以将其连接到一个DI容器中一次,并在各处使用相同的接口,而MockupFactory针对特定类的接口则不太可能可以在更多地方使用-与分隔良好的接口相比,这意味着需要更多的配置。
丹丹
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.