繁体   English   中英

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

[英]Unit testing a class with a Java 8 Clock

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对象,但能够让一个对象看到两个不同的时钟值(就好像它是一个常规的系统时钟滴答作响)。 因此,我无法将固定时钟传递给构造函数。

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

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

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

让我将 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'
    }
}

单元测试:

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

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

假设这个类已经被修改为在构造函数中采用一个时钟,并在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 更好地进行快进、倒带和时间旅行。

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

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

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

另一种更明确、更容易理解并且可能比模拟更健壮的替代方法是创建一个真正的Clock实现,它是mutable ,以便测试可以注入它并根据需要修改它。 这个不难实现,或者这里有几个现成的实现:

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

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

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

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

我遇到了同样的问题,无法使用简单且有效的现有解决方案,因此我最终遵循了代码。 当然,它可以更好,具有可配置的 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++]
        }
    }
}

您可以给MyClass一个时钟,一个模拟Clock ,或者获取一个时钟-没有更多选择。

你不需要自己做的反射,你用嘲弄的库。

关于“正确的”方法是什么,意见不一。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM