使用 Java 8 时钟对类进行单元测试

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/27067049/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 03:46:14  来源:igfitidea点击:

Unit testing a class with a Java 8 Clock

javaunit-testingjava-8java-time

提问by Mike

Java 8 introduced java.time.Clockwhich can be used as an argument to many other java.timeobjects, allowing you to inject a real or fake clock into them. For example, I know you can create a Clock.fixed()and then call Instant.now(clock)and it will return the fixed Instantyou provided. This sounds perfect for unit testing!

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

However, I'm having trouble figuring out how best to use this. I have a class, similar to the following:

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

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

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

Now, I want to unit test this code. I need to be able to set clockto produce fixed times so that I can test method()at different times. Clearly, I could use reflection to set the clockmember to specific values, but it would be nice if I didn't have to resort to reflection. I could create a public setClock()method, but that feels wrong. I don't want to add a Clockargument to the method because the real code shouldn't be concerned with passing in a clock.

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

What is the best approach for handling this? This is new code so I could reorganize the class.

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

Edit: To clarify, I need to be able to construct a single MyClassobject but be able to have that one object see two different clock values (as if it were a regular system clock ticking along). As such, I cannot pass a fixed clock into the constructor.

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

采纳答案by Xiao

Let me put Jon Skeet's answer and the comments into code:

让我将 Jon Skeet 的回答和评论放入代码中:

class under test:

被测类:

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'
    }
}

unit test:

单元测试:

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 
    }
}

回答by Jon Skeet

I don't want to add a Clock argument to the method because the real code shouldn't be concerned with passing in a clock.

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

No... but you might want to consider it as a constructorparameter. Basically you're saying that your class needs a clock with which to work... so that's a dependency. Treat it as you would any other dependency, and inject it either in a constructor or via a method. (I personally favour constructor injection, but YMMV.)

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

As soon as you stop thinking of it as something you can easily construct yourself, and start thinking of it as "just another dependency" then you can use familiar techniques. (I'm assuming you're comfortable with dependency injection in general, admittedly.)

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

回答by Dave Newton

You either give MyClassa clock, mock Clock, orretrieve a clock–there aren't many more options.

你要么给MyClass一个时钟,模拟Clock,要么检索一个时钟——没有更多的选择了。

You don't need to do the reflection yourself, you use a mocking library.

您不需要自己进行反射,您可以使用模拟库。

Opinions vary on what the "correct" approach is.

关于什么是“正确”方法的意见各不相同。

回答by Rik

I'm a bit late to the game here, but to add to the other answers suggesting using a Clock - this definitely works, and by using Mockito's doAnswer you can create a Clock which you can dynamically adjust as your tests progress.

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

Assume this class, which has been modified to take a Clock in the constructor, and reference the clock on Instant.now(clock)calls.

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

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

    // ...

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

Then, in your test setup:

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

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);
}

Later in your test:

稍后在您的测试中:

@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
}

You're telling Mockito to always run a function which returns the current value of currentTime whenever instant() is called. Instant.now(clock)will call clock.instant(). Now you can fast-forward, rewind, and generally time travel better than a DeLorean.

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

回答by Raman

To start, definitely inject a Clockinto your class under test, as recommended by @Jon Skeet. If your class only requires one time, then simply pass in a Clock.fixed(...)value. However, if your class behaves differently across time e.g. it does something at time A, and then does something else at time B, then note that the clocks created by Java are immutable, and thus cannot be changed by the test to return time A at one time, and then time B at another.

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

Mocking, as per the accepted answer, is one option, but does tightly couple the test to the implementation. For example, as one commenter points out, what if the class under test calls LocalDateTime.now(clock)or clock.millis()instead of clock.instant()?

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

An alternate approach that is a bit more explicit, easier to understand, and may be more robust than a mock, is to create a real implementation of Clockthat is mutable, so that the test can inject it and modify it as necessary. This is not difficult to implement. See https://github.com/robfletcher/test-clockfor a good example of this.

另一种方法就是有点更明确,更容易理解,而且可能比模拟更强大的,是创建一个真正的实现Clock可变的,所以测试可以注入,并修改是必要的。这并不难实现。请参阅https://github.com/robfletcher/test-clock的一个很好的例子。

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(...)

回答by Matthew

As others have noted, you need to mock it somehow - however it's pretty easy to roll your own:

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

    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();
        }
    }