Подтвердить что ты не робот

Единичное тестирование класса с помощью 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, но иметь возможность иметь этот один объект, чтобы увидеть два разных значения часов (как если бы это был обычный системный тактовый сигнал). Таким образом, я не могу передать фиксированные часы в конструктор.

4b9b3361

Ответ 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, либо получаете часы - не так много вариантов.

Вам не нужно делать размышления самостоятельно, вы используете насмешливую библиотеку.

Мнения различаются в зависимости от того, что такое "правильный" подход.