【问题标题】:Unit testing a class with a Java 8 Clock使用 Java 8 时钟对类进行单元测试
【发布时间】:2016-07-22 15:21:23
【问题描述】:

Java 8 引入了java.time.Clock,它可以用作许多其他java.time 对象的参数,允许您向它们注入真实或虚假的时钟。例如,我知道您可以创建一个Clock.fixed(),然后调用Instant.now(clock),它将返回您提供的固定Instant。这听起来很适合单元测试!

但是,我无法弄清楚如何最好地使用它。我有一个类,类似于以下内容:

public class MyClass {
    private Clock clock = Clock.systemUTC();

    public void method1() {
        Instant now = Instant.now(clock);
        // Do something with 'now'
    }
}

现在,我想对这段代码进行单元测试。我需要能够设置clock 以产生固定时间,以便我可以在不同时间测试method()。显然,我可以使用反射将clock 成员设置为特定值,但如果我不必求助于反射就好了。我可以创建一个公共的setClock() 方法,但感觉不对。我不想在方法中添加Clock 参数,因为真正的代码不应该关心传递时钟。

处理此问题的最佳方法是什么?这是新代码,所以我可以重新组织课程。

编辑:澄清一下,我需要能够构造单个 MyClass 对象,但能够让该对象看到两个不同的时钟值(就好像它是一个常规的系统时钟滴答作响)。因此,我无法将固定时钟传递给构造函数。

【问题讨论】:

  • 现在,我想对这段代码进行单元测试。 您应该指出您期望 MyClass 的行为类型。这将为此处遵循的方法提供信息。
  • 跟进:我认为它基本上归结为你不能真正使用Clock.fixed 进行单元测试,就像我希望的那样。将需要正常的模拟方法。

标签: java unit-testing java-8 java-time


【解决方案1】:

我不想在方法中添加 Clock 参数,因为真正的代码不应该关心传递时钟。

不...但您可能希望将其视为 构造函数 参数。基本上你是说你的班级需要一个时钟来工作......所以这是一个依赖。像对待任何其他依赖项一样对待它,并将其注入构造函数或通过方法。 (我个人更喜欢构造函数注入,但是YMMV。)

一旦您不再将其视为可以轻松构建自己的东西,并开始将其视为“只是另一个依赖项”,那么您就可以使用熟悉的技术。 (诚​​然,我假设您通常对依赖注入感到满意。)

【讨论】:

  • 是的,我考虑过这个。但是,问题是通过构造函数传递时钟让我将其设置为单个固定时钟。我想我没有说清楚,但是我需要能够在单个测试中将时钟设置为多个值,而无需构造新对象。
  • @Mike 请更新您的示例以明确您的用例是什么。
  • @Mike 您应该能够模拟时钟作为参数传递(例如使用 Mockito),而不是提供固定的时钟。像这样,您可以在不同的调用中返回不同的值。
  • @Mike:或者代替模拟,您可以编写自己的可重复使用的假时钟,具有您想要的任何行为 - 例如每次调用增加 1 秒。但是,此时您将依赖于您在生产代码中对时钟进行的调用次数,这并不理想。
  • 在 Puce 的回答之上,您应该能够根据 mockito.googlecode.com/svn/tags/1.8.5/javadoc/org/mockito/… 使模拟返回不同的值
【解决方案2】:

让我将 Jon Skeet 的答案和 cmets 放入代码中:

待测类:

public class Foo {
    private final Clock clock;
    public Foo(Clock clock) {
        this.clock = clock;
    }

    public void someMethod() {
        Instant now = clock.instant();   // this is changed to make test easier
        System.out.println(now);   // Do something with 'now'
    }
}

单元测试:

public class FooTest() {

    private Foo foo;
    private Clock mock;

    @Before
    public void setUp() {
        mock = mock(Clock.class);
        foo = new Foo(mock);
    }

    @Test
    public void ensureDifferentValuesWhenMockIsCalled() {
        Instant first = Instant.now();                  // e.g. 12:00:00
        Instant second = first.plusSeconds(1);          // 12:00:01
        Instant thirdAndAfter = second.plusSeconds(1);  // 12:00:02

        when(mock.instant()).thenReturn(first, second, thirdAndAfter);

        foo.someMethod();   // string of first
        foo.someMethod();   // string of second
        foo.someMethod();   // string of thirdAndAfter 
        foo.someMethod();   // string of thirdAndAfter 
    }
}

【讨论】:

  • 您可能想要更改 firstsecond 等以满足您的需求(例如,时间需要过去),但您明白了。
  • 在实践中,上面的示例测试会遇到 NPE,因为用户代码很可能会调用 LocalDateTime.now(clock) 而不是 clock.instant()NullPointerException 将在LocalDateTime 调用clock.getZone().getRules() 的那一刻被抛出。相反,应该通过调用Clock.fixed(Instant,ZoneId) 来创建一个真实 Clock 对象。
  • @Rogério 我对这个问题的解释是“我如何模拟在连续调用中返回不同的值”,这就是答案。对于LocalDateTime.now()Instant.now() 部分,我对java 8 time 的了解不够多,但是如果这里需要静态方法,可能需要将其包装到一个类中,因为PowerMock/EasyMock does not support Stubbing consecutive callsMockito
  • 正如@Rogério 指出的那样,这是通过假设被测类查询Clock.instant() 来实现的。如果更改为查询Clock.millis(),则将始终返回0。这不好。无论实际调用的方法如何,都需要模拟 clock 对象返回模拟时间。
  • 使用真正的时钟和Clock.fixed 而不是嘲笑!
【解决方案3】:

我在这里玩游戏有点晚了,但要添加到建议使用时钟的其他答案 - 这绝对有效,并且通过使用 Mockito 的 doAnswer 您可以创建一个时钟,您可以随着测试的进行动态调整它。

假设这个类已被修改为在构造函数中使用 Clock,并在 Instant.now(clock) 调用时引用时钟。

public class TimePrinter() {
    private final Clock clock; // init in constructor

    // ...

    public void printTheTime() {
        System.out.println(Instant.now(clock));
    }
}

然后,在您的测试设置中:

private Instant currentTime;
private TimePrinter timePrinter;

public void setup() {
   currentTime = Instant.EPOCH; // or Instant.now() or whatever

   // create a mock clock which returns currentTime
   final Clock clock = mock(Clock.class);
   when(clock.instant()).doAnswer((invocation) -> currentTime);

   timePrinter = new TimePrinter(clock);
}

稍后在您的测试中:

@Test
public void myTest() {
    myObjectUnderTest.printTheTime(); // 1970-01-01T00:00:00Z

    // go forward in time a year
    currentTime = currentTime.plus(1, ChronoUnit.YEARS);

    myObjectUnderTest.printTheTime(); // 1971-01-01T00:00:00Z
}

您告诉 Mockito 始终运行一个函数,该函数在调用 instant() 时返回 currentTime 的当前值。 Instant.now(clock) 将调用 clock.instant()。现在,您可以比 DeLorean 更好地快进、快退和一般时间旅行。

【讨论】:

  • 这个答案很完美。
  • 我的用例是在一些测试中快进几秒钟以验证一些基于时间的验证规则。这对我来说似乎是最简单、最优雅的解决方案。完美,非常感谢。 DeLorean 的另一个 +1。
  • 真正优雅的解决方案 :) DeLorean 参考 +1。
  • 我正在使用 Mockito 1.10.19 并且 doAnswer 不存在。我将其替换为thenAnswer
【解决方案4】:

创建一个可变时钟而不是模拟

首先,按照@Jon Skeet 的建议,绝对将Clock 注入您的测试类。如果您的课程只需要一次,那么只需传入一个Clock.fixed(...) 值。但是,如果您的班级随着时间的推移表现不同,例如它在时间 A 做某事,然后在时间 B 做其他事情,然后注意 Java 创建的时钟是不可变,因此不能通过测试更改为一次返回时间 A,然后在另一个时间 B。

根据公认的答案,模拟是一种选择,但确实将测试与实现紧密结合。例如,正如一位评论者指出的那样,如果被测类调用LocalDateTime.now(clock)clock.millis() 而不是clock.instant() 怎么办?

另一种更明确、更易于理解并且可能比模拟更健壮的替代方法是创建Clock 的真正实现,即可变,这样test 可以注入它并根据需要对其进行修改。这个实现起来不难,或者这里有几个现成的实现:

下面是在测试中如何使用这样的东西:

MutableClock c = new MutableClock(Instant.EPOCH, ZoneId.systemDefault());
ClassUnderTest classUnderTest = new ClassUnderTest(c);

classUnderTest.doSomething()
assertTrue(...)

c.instant(Instant.EPOCH.plusSeconds(60))

classUnderTest.doSomething()
assertTrue(...)

【讨论】:

  • 替代实现,已发布到 maven repo:github.com/Mercateo/test-clock
  • 来自documented implementation requirements of the Clock abstract class:“这个抽象类必须小心实现以确保其他类正确运行。所有可以实例化的实现都必须是最终的、不可变的和线程安全的 i>。”
  • @AndrewF 鉴于实现的目的不是通用时钟,而是作为单元测试的有限用途时间源,我不会对违反该要求感到内疚。
  • Mocking 很好,但也有限;该测试具有如何使用Clock 的特殊知识(请参阅许多关于此效果的cmets)。 Windable/mutable Clock 是一个通用测试工具,ThreeTen 版本由 java.time.Clock 的作者编写
【解决方案5】:

正如其他人所指出的,您需要以某种方式模拟它 - 但是很容易自己滚动:

    class UnitTestClock extends Clock {
        Instant instant;
        public void setInstant(Instant instant) {
            this.instant = instant;
        }

        @Override
        public Instant instant() {
            return instant;
        }

        @Override
        public ZoneId getZone() {
            return ZoneOffset.UTC;
        }

        @Override
        public Clock withZone(ZoneId zoneId) {
            throw new UnsupportedOperationException();
        }
    }

【讨论】:

  • 已经有一个类,在 JDK 中提供,见Clock.fixed()
  • @KrzysztofWolny,我认为你错过了重点——这实际上是在创建一个可变时钟,就像拉曼的回答一样。
【解决方案6】:

我遇到了同样的问题,并且无法使用简单且有效的现有解决方案,因此我最终遵循了代码。当然,它可以更好,具有可配置的 TZ 等,但我需要一些易于在测试中使用的东西,我想在其中检查我的被测类如何处理时钟:

  1. 我不想关心 Clock 的哪个特定方法被调用,所以模拟不是一种方法。
  2. 我想预先定义毫厘

注意:这个类是 Groovy 类,用于 Spock 测试,但很容易翻译成 Java。

class FixedTicksClock extends Clock {
    private ZoneId systemDefault = ZoneId.systemDefault()
    private long[] ticks
    private int current = 0

    FixedTicksClock(long ... ticks) {
        this.ticks = ticks
    }

    @Override
    ZoneId getZone() {
        systemDefault
    }

    @Override
    Clock withZone(final ZoneId zone) {
        systemDefault = zone
        this
    }

    @Override
    Instant instant() {
        ofEpochMilli(getNextTick())
    }

    @Override
    long millis() {
        getNextTick()
    }

    private long getNextTick() {
        if (current >= ticks.length) {
            throw new IllegalStateException('No more ticks provided')
        } else {
            ticks[current++]
        }
    }
}

【讨论】:

    猜你喜欢
    • 2018-11-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-10
    相关资源
    最近更新 更多