Единичное тестирование класса с помощью 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'
}
}
Теперь я хочу unit test этот код. Мне нужно установить clock
для создания фиксированных времен, чтобы я мог тестировать method()
в разное время. Ясно, что я мог бы использовать отражение, чтобы установить член clock
на определенные значения, но было бы неплохо, если бы мне не пришлось прибегать к размышлениям. Я мог бы создать общедоступный метод setClock()
, но это не так. Я не хочу добавлять аргумент clock
к методу, потому что реальный код не должен касаться передачи в часах.
Каков наилучший подход для решения этой проблемы? Это новый код, поэтому я могу реорганизовать класс.
Изменить. Чтобы уточнить, мне нужно иметь возможность создать один объект MyClass
, но иметь возможность иметь этот один объект, чтобы увидеть два разных значения часов (как если бы это был обычный системный тактовый сигнал). Таким образом, я не могу передать фиксированные часы в конструктор.
Ответы
Ответ 1
Позвольте мне ответить 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'
}
}
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
}
}
Ответ 2
Я не хочу добавлять аргумент Clock к методу, потому что реальный код не должен касаться передачи в часах.
Нет... но вы можете рассмотреть его как параметр конструктора. В основном вы говорите, что вашему классу нужны часы, с которыми нужно работать... так что зависимость. Относитесь к нему так же, как и к любой другой зависимости, и вводите его либо в конструктор, либо через метод. (Я лично одобряю инъекцию конструктора, но YMMV.)
Как только вы перестанете думать об этом как о чем-то, что вы можете легко построить, и начните думать об этом как о "просто другой зависимости", тогда вы можете использовать знакомые методы. (Я предполагаю, что вам комфортно с инъекцией зависимости в целом, по общему признанию.)
Ответ 3
Я немного опоздал на игру здесь, но добавлю к другим ответам, предлагающим использовать Clock - это определенно работает, и с помощью 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 всегда запускает функцию, которая возвращает текущее значение currentTime всякий раз, когда вызывается функция instant(). Instant.now(clock)
вызовет clock.instant()
. Теперь вы можете перемотки вперед, назад и вообще путешествовать во времени лучше, чем DeLorean.
Ответ 4
Для начала обязательно добавьте Clock
в тестируемый класс, как рекомендует @Jon Skeet. Если вашему классу требуется только один раз, просто передайте значение Clock.fixed(...)
. Однако, если ваш класс ведет себя по-разному во времени, например, он что-то делает во время A, а затем делает что-то другое во время B, то обратите внимание, что часы, созданные Java, являются неизменяемыми и, следовательно, не могут быть изменены тестом для возврата времени A в один раз, а затем время Б в другой.
Насмешка, согласно принятому ответу, является одним из вариантов, но она тесно связывает тест с реализацией. Например, как указывает один комментатор, что, если тестируемый класс вызывает LocalDateTime.now(clock)
или clock.millis()
вместо clock.instant()
?
Альтернативный подход, который является немного более явным, более простым для понимания и может быть более надежным, чем фиктивный, состоит в том, чтобы создать реальную реализацию 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(...)
Ответ 5
Вы либо даете MyClass
часы, mock Clock
, либо получаете часы - не так много вариантов.
Вам не нужно делать размышления самостоятельно, вы используете насмешливую библиотеку.
Мнения различаются в зависимости от того, что такое "правильный" подход.