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

Mik*_*ike 60 java unit-testing java-8 java-time

引入了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'
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

Jon*_*eet 46

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

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

一旦你不再把它想象成你可以很容易地构建自己的东西,并开始把它想象成"只是另一个依赖",那么你可以使用熟悉的技术.(诚​​然,我假设你对依赖注射很满意.)

  • @Mike你应该能够模仿你传递的时钟作为参数(例如使用Mockito)而不是提供固定的时钟.像这样,您可以在不同的呼叫中返回不同的值. (3认同)
  • @Mike:或者不是模拟,你可以编写自己的可重复使用的假时钟,无论你想要什么行为 - 比如每次调用增加1秒.但是,在那时,您将依赖于您在生产代码中对时钟进行的调用次数,这并不理想. (3认同)
  • 是的,我考虑过这个。但是,问题是通过构造函数传递时钟可以让我将其设置为单个固定时钟。我想我没有说清楚,但我需要能够在单个测试中将时钟设置为多个值,而无需构建新对象。 (2认同)
  • 除了Puce的答案之外,你应该能够使模拟返回不同的值,每个http://mockito.googlecode.com/svn/tags/1.8.5/javadoc/org/mockito/Mockito.html#10 (2认同)

Jef*_*iao 45

让我把Jon Skeet的答案和评论放到代码中:

被测试的班级:

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'
    }
}
Run Code Online (Sandbox Code Playgroud)

单元测试:

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 
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 在实践中,上面的示例测试将遇到NPE,因为用户代码很可能会调用`LocalDateTime.now(clock)`而不是`clock.instant()`; 一个`NullPointerException`将在`LocalDateTime`调用`clock.getZone().getRules()`时被抛出.相反,应该通过调用`Clock.fixed(Instant,ZoneId)`来创建一个*real*`Clock`对象. (24认同)
  • 使用带有`Clock.fixed`的真实时钟而不是模拟! (6认同)
  • 您可能想更改“first”、“second”等以满足您的需要(例如,时间需要是过去的时间),但您明白了。 (2认同)
  • @Rogério 我对这个问题的解释是“我如何模拟在连续调用中返回不同的值”,这就是答案。对于`LocalDateTime.now()` 或`Instant.now()` 部分,我对java 8 时间了解不够,但是如果这里需要静态方法,则可能需要将其包装到一个类中,因为PowerMock/ EasyMock [不支持](http://easymock.org/api/org/easymock/IExpectationSetters.html#andReturn-T-) 在 [Mockito](http://mockito.googlecode.com/ svn/tags/1.8.5/javadoc/org/mockito/Mockito.html#10) (2认同)
  • 正如@Rogério指出的那样,这是基于以下假设的:测试中的类查询`Clock.instant()`。如果更改为查询`Clock.millis()`,它将始终返回'0'。不好 有一个需要模拟的`clock`对象,无论实际调用什么方法,它都返回模拟时间。 (2认同)

Rik*_*Rik 25

我在游戏中有点迟了,但是要添加其他答案建议使用时钟 - 这绝对有效,并且通过使用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));
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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);
}
Run Code Online (Sandbox Code Playgroud)

在你的测试后期:

@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
}
Run Code Online (Sandbox Code Playgroud)

你告诉Mockito总是运行一个函数,只要调用instant(),就会返回currentTime的当前值. Instant.now(clock)会打电话clock.instant().现在,您可以比DeLorean更快地前进,快退和一般旅行.

  • 我正在使用Mockito 1.10.19并且`doAnswer`不存在.我用`thenAnswer`替换它 (5认同)

Ram*_*man 10

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

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

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

另一种方法就是有点更明确,更容易理解,而且可能比模拟更强大的,是创建一个真正的实现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(...)
Run Code Online (Sandbox Code Playgroud)

  • @AndrewF 鉴于实现的目的不是通用时钟,而是作为单元测试的有限用途时间源,我不会因为违反该要求而感到内疚。 (2认同)

Mat*_*hew 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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

  • @KrzysztofWolny,我认为你错过了重点——这实际上是在创建一个可变时钟,就像拉曼的答案一样。 (7认同)
  • JDK 中已经提供了一个类,请参阅“Clock.fixed()” (2认同)