【问题标题】:Time dependent unit tests时间相关的单元测试
【发布时间】:2011-08-03 02:13:17
【问题描述】:

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

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

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

【问题讨论】:

  • 对于某些函数,最简单的解决方案是将当前时间作为参数传递。
  • 直到由于夏令时突然单元测试失败;)
  • 就像模拟一样,它不必是实际的当前时间。你可以硬编码一个安全的瞬间。

标签: java junit


【解决方案1】:

使您的代码可测试的最佳方法 (IMO) 是将“当前时间是多少”的依赖项提取到它自己的接口中,其中一个实现使用当前系统时间(正常使用),一个实现让您设置时间,根据需要提前等。

我在各种情况下都使用过这种方法,而且效果很好。它很容易设置 - 只需创建一个界面(例如Clock),它有一个方法可以以您想要的任何格式为您提供当前瞬间(例如使用 Joda Time,或者可能是 Date)。

【讨论】:

  • Joda time 内置了对该抽象的支持(请参阅我的回答),因此您不必在代码中引入它。
  • @Laurent:我认为这实际上并没有那么优雅。从根本上说,我认为诸如“获取当前时间”之类的服务 一个依赖项(就像我将随机数生成视为一个依赖项一样),所以我认为明确这一点是件好事。这意味着您也可以并行化测试等。
  • 点了。不要误会我的意思:我通常赞成抽象。但是,当前时间的概念在实际项目中可能难以抽象,尤其是在使用不抽象此概念的第三方库时。
  • @Laurent:哦,是的,当你使用第三方库时,这很棘手......但是你会遇到与 setCurrentMillisFixed 相同的问题,除非你的第三方库碰巧使用了 Joda Time : (
  • @Jon:我完全同意你的看法。现在 Java 8 有了抽象类 Clock。
【解决方案2】:

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

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

【讨论】:

  • 但这些是静态方法——您将通过这种方式引入单元测试之间的依赖关系。因此,我更喜欢 Jon Skeets 解决方案。
  • hstoerr:我看不出测试之间会有怎样的依赖关系,除非它们要在不同的线程中执行(这里可能不是这种情况)。但即便如此,Joda Time 也提供了DateTimeUtils.setCurrentMillisProvider(DateTimeUtils.MillisProvider) 方法,它肯定允许线程绑定的实现。
【解决方案3】:

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

【讨论】:

  • 如果您只需要能够询问“现在几点了?”,您就不需要一个完整的时钟;您可能更喜欢使用感兴趣的时间单位的Supplier(例如LocalDateTime)。
  • @Jubobs 如果没有将Clock 作为参数传递给now(),您将无法控制LocalDateTime.now() 返回的当前时间。
  • @deamon 我的建议是有一个Supplier<LocalDateTime> 类型的端口,你可以在其中插入一个真正的适配器——例如。 LocalDateTime::now——或假适配器——例如() -> LocalDateTime.of(2017, 10, 24, 0, 0)——在方便的地方(单元测试)。不需要整个Clock
【解决方案4】:

要添加到Jon Skeet's answer,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);

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

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

【讨论】:

    【解决方案5】:

    我使用类似于 Jon 的方法,但不是为当前时间创建专门的接口(例如,Clock),我通常创建一个特殊的测试接口(例如,MockupFactory)。我把测试代码所需的所有方法都放在那里。例如,在我的一个项目中,我有四种方法:

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

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

    如果我需要更多模拟对象,我只需向该接口添加一个方法并在测试和实际实现中实现它。

    通过这种方式,我首先设计了适合测试的代码,而不会对代码本身施加太多影响。事实上,这样代码变得更加简洁,因为很多工厂代码都集中在一个地方。例如,如果我需要在实际代码中切换到另一个数据库客户端实现,我只需要修改一行,而不是四处寻找对构造函数的引用。

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

    【讨论】:

    • 这听起来像是要创建一个新接口来封装每个被测类的依赖关系?这将需要至少一个实现。那么现在对于每个被测试的类,你都有类,至少一个测试类,依赖分组接口,以及该接口的实现?我真的不喜欢那样。而且我觉得在你的代码和它的实际依赖关系之间添加额外的间接级别(即,在查看你的类之后,我还必须查看依赖接口)实际上使代码更难理解。
    • @Dathan,不,不是每个班级。只有那些具有在测试期间必须模拟的依赖项。在我的应用程序中,恰好只有一个这样的类。此外,课程的数量并不意味着什么。如果实现只是class DefaultMockupFactory implements MockupFactory {Timer createTimer() {return new Timer();}},那并没有那么复杂,是吗?在代码中的某处有factory.createTimer() 也不会使代码更难理解。但我同意在某些情况下这可能不是最好的方法。
    • 不,我不认为它增加了太多的复杂性——只是不必要的复杂性。我觉得通过这个外观接口注入的额外间接级别可能会抑制代码的可读性。例如,如果我有您的代码示例,但 MockupFactory 有一些其他方法,并且我想找到代码中使用 createTimer() 的所有位置,我必须从被测类导航到接口,然后搜索该方法的用途,而不仅仅是在类中搜索。
    • @Dathan,接口是嵌套的(具有包私有可见性),所以它仍然在类中。无论如何,我和 Jon 的方法之间的区别在于,我对所有必须模拟的依赖项使用单个接口 (MockupFactory),而 Jon 建议为每个依赖项设置一个单独的接口 (TimerFactory 等等) .你将如何在没有任何接口的情况下测试代码对我来说是个谜。无论哪种方式,都需要额外的复杂性。
    • 我同意,一些接口需要添加。但我更愿意遵循接口隔离——所以在这种情况下,我更喜欢 Jon 的方法。部分原因是系统中重用了许多接口——TimerFactory 很可能会被重用,因此您可以将它连接到 DI 容器中一次,并在任何地方使用相同的接口,而 MockupFactory 用于一个特定的类不太可能在更多地方使用——这意味着与隔离良好的接口相比需要更多的配置。
    猜你喜欢
    • 1970-01-01
    • 2018-11-09
    • 2019-08-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-09
    • 1970-01-01
    相关资源
    最近更新 更多