Залежні від часу одиничні тести


75

Мені потрібно протестувати функцію, результат якої буде залежати від поточного часу (з використанням часу Joda isBeforeNow()це трапляється).

public boolean isAvailable() {
    return (this.someDate.isBeforeNow());
}

Чи можна заглушити / висміяти системний час за допомогою (наприклад, використовуючи Mockito), щоб я міг надійно перевірити функцію?


2
Для деяких функцій найпростішим рішенням є передача поточного часу як параметра.
DariusL

Поки ви раптом не отримаєте невдалий одиничний тест через перехід на літній час;)
pojo

1
Як і з глузуванням, це не повинен бути фактичний поточний час. Ви можете жорстко закодувати безпечний час.
DariusL

Відповіді:


64

Joda time підтримує встановлення "фальшивого" поточного часу за допомогою setCurrentMillisFixedі setCurrentMillisOffsetметодів DateTimeUtilsкласу.

Див. Https://www.joda.org/joda-time/apidocs/org/joda/time/DateTimeUtils.html


11
Але це статичні методи - ви таким чином введете залежності між unittests. Таким чином, я віддаю перевагу рішенню Jon Skeets.
Hans-Peter Störr,

hstoerr: Я не бачу, як би існували залежності між тестами, за винятком випадків, коли вони мали виконуватися в різних потоках (що, мабуть, тут не так). Але навіть тоді Joda Time надає DateTimeUtils.setCurrentMillisProvider(DateTimeUtils.MillisProvider)метод, який, безсумнівно, дозволить реалізацію, пов'язану з потоками.
Rogério

128

Найкращий спосіб (IMO) зробити свій код перевіряемым - це виділити залежність "який поточний час" у власний інтерфейс, з реалізацією, яка використовує поточний системний час (зазвичай використовується), і реалізацією, яка дозволяє встановити час , просувайте його як завгодно і т.д.

Я використовував цей підхід у різних ситуаціях, і він працював добре. Це легко налаштувати - просто створіть інтерфейс (наприклад Clock), який має єдиний метод, щоб надати вам поточний момент у будь-якому форматі, який ви хочете (наприклад, використовуючи Joda Time, або, можливо, a Date).


7
Joda time має вбудовану підтримку для цієї абстракції (див. Мою відповідь), тому вам не потрібно вводити її у свій код.
Лоран Пірейн,

21
@ Лоран: Я не думаю, що насправді це майже настільки ж елегантно. По суті, я думаю, що така послуга, як "отримання поточного часу", є залежністю (подібно до того, як я розглядаю генерацію випадкових чисел як залежність), тому я вважаю, що це добре чітко вказати. Це означає, що ви також можете розпаралелювати тести тощо.
Джон Скіт,

5
Очко взяте. Не зрозумійте мене неправильно: я зазвичай за абстракцію. Однак поняття поточного часу може бути важко абстрагувати у реальному проекті, особливо коли використовуються сторонні бібліотеки, які не абстрагують це поняття.
Лоран Пірейн,

3
@Laurent: О так, коли ви використовуєте сторонні бібліотеки, це складно ... але тоді у вас буде така сама проблема з setCurrentMillisFixed, якщо ваша стороння бібліотека випадково не використовує Joda Time :(
Джон Скіт,

3
@ Джон: Я повністю з вами згоден. Тепер Java 8 має абстрактний клас Clock.
xli

22

Java 8 представила абстрактний клас, java.time.Clockякий дозволяє використовувати альтернативну реалізацію для тестування. Це саме те, що Джон запропонував у своїй відповіді тоді.


1
Якщо вам потрібна лише можливість запитати "Який час зараз?", Вам не потрібні цілі годинники; Ви можете замість цього скористатися Supplierодиницею часу, що цікавить (наприклад LocalDateTime).
jub0bs

1
@Jubobs Ви не можете контролювати поточний час, який повертається, LocalDateTime.now()не Clockпередавши його як параметр now().
Дімон

1
@deamon Я пропоную мати порт типу Supplier<LocalDateTime>, до якого ви можете підключити або справжній адаптер - наприклад LocalDateTime::now- або підроблений адаптер - наприклад - () -> LocalDateTime.of(2017, 10, 24, 0, 0)де зручно (модульні тести). Не потрібно цілого Clock.
jub0bs

4

Щоб додати до відповіді Джона Скіта , Joda Time вже містить поточний інтерфейс часу: DateTimeUtils.MillisProvider

Наприклад:

import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils.MillisProvider;

public class Check {
    private final MillisProvider millisProvider;
    private final DateTime someDate;

    public Check(MillisProvider millisProvider, DateTime someDate) {
        this.millisProvider = millisProvider;
        this.someDate = someDate;
    }

    public boolean isAvailable() {
        long now = millisProvider.getMillis();
        return (someDate.isBefore(now));
    }
}

Здійснюйте час в юніт-тесті (використовуючи Mockito, але ви можете реалізувати свій власний клас MillisProviderMock):

DateTime fakeNow = new DateTime(2016, DateTimeConstants.MARCH, 28, 9, 10);
MillisProvider mockMillisProvider = mock(MillisProvider.class);
when(mockMillisProvider.getMillis()).thenReturn(fakeNow.getMillis());

Check check = new Check(mockMillisProvider, someDate);

Використовуйте поточний час у виробництві ( DateTimeUtils.SYSTEM_MILLIS_PROVIDER додано до Joda Time у 2.9.3):

Check check = new Check(DateTimeUtils.SYSTEM_MILLIS_PROVIDER, someDate);

1

Я використовую підхід, схожий на підхід Джона, але замість того, щоб створювати спеціалізований інтерфейс лише на поточний час (скажімо, Clock), я зазвичай створюю спеціальний інтерфейс тестування (скажімо, MockupFactory). Я помістив туди всі методи, які мені потрібні для тестування коду. Наприклад, в одному зі своїх проектів у мене є чотири методи:

  • той, який повертає макет клієнта бази даних;
  • той, який створює макет об’єкта-сповіщувача, який повідомляє код про зміни в базі даних;
  • той, який створює макет java.util.Timer, який запускає завдання, коли я цього хочу;
  • той, що повертає поточний час.

Тестований клас має конструктор, який приймає цей інтерфейс серед інших аргументів. Той без цього аргументу просто створює екземпляр цього інтерфейсу за замовчуванням, який працює "в реальному житті". І інтерфейс, і конструктор є приватними для пакету, тому API тестування не витікає за межі пакету.

Якщо мені потрібні більш імітовані об'єкти, я просто додаю метод до цього інтерфейсу та впроваджую його як у тестуванні, так і в реальних реалізаціях.

Таким чином я розробляю код, придатний для тестування, в першу чергу, не накладаючи занадто багато на сам код. Насправді, таким чином код стає ще чистішим, оскільки багато заводських кодів зібрано в одному місці. Наприклад, якщо мені потрібно перейти до іншої реалізації клієнта бази даних у реальному коді, мені потрібно лише змінити лише один рядок, замість того, щоб шукати посилання на конструктор.

Звичайно, як і у випадку з підходом Джона, він не буде працювати зі стороннім кодом, який ви не можете або не можете змінити.


Це схоже на те, що ви збираєтеся створити новий інтерфейс для інкапсуляції залежностей кожного тестованого класу? Для чого тоді знадобиться принаймні одне впровадження. Отже, для кожного тестованого класу у вас є клас, принаймні один тестовий клас, інтерфейс групування залежностей та реалізація цього інтерфейсу? Мені це дуже не подобається. І мені хочеться додати, що додатковий рівень опосередкованості між вашим кодом та його реальними залежностями (тобто, після перегляду вашого класу я повинен також поглянути на інтерфейс залежностей) насправді ускладнює розуміння коду.
Dathan

@Dathan, ні, не кожен клас. Тільки ті, які мають залежності, які слід емулювати під час тестування. У моєму додатку буває лише один такий клас. Крім того, кількість занять нічого не означає. Якщо впровадження просто class DefaultMockupFactory implements MockupFactory {Timer createTimer() {return new Timer();}}це не така вже й складна справа, чи не так? А наявність factory.createTimer()десь у коді теж не ускладнює розуміння коду. Але я згоден, що в деяких випадках це може бути не найкращим способом це зробити.
Сергій Таченов

Ні, я не думаю, що це додає занадто багато складності - просто непотрібної складності. Я відчуваю, що додатковий рівень опосередкованості, введений завдяки наявності цього фасадного інтерфейсу, може заважати читабельності коду. Наприклад, якщо я мав зразок вашого коду, але MockupFactoryмав на ньому кілька інших методів, і я хотів знайти всі місця в коді, де createTimer()був використаний, я повинен перейти від тестованого класу до інтерфейсу, а ПОТІ для використання методу, а не просто пошуку в класі.
Dathan

@Dathan, інтерфейс вкладений (з приватною видимістю пакунків), тому все це всередині класу. І в будь-якому випадку, різниця між моїм та підходом Джона полягає в тому, що я використовую єдиний інтерфейс ( MockupFactory) для всіх залежностей, які повинні бути емульовані, тоді як Джон пропонує мати окремий інтерфейс для кожної залежності ( TimerFactoryі так далі). Як би ви протестували код взагалі без будь-якого інтерфейсу, для мене загадка. Так чи інакше, необхідна додаткова складність.
Сергій Таченов

1
Я згоден, потрібно додати якийсь інтерфейс. Але я б набагато дотримувався сегрегації інтерфейсу - тому в цьому випадку я б віддав перевагу підходу Джона. Частково це те, що багато інтерфейсів повторно використовуються навколо системи - цілком ймовірно, TimerFactoryвони будуть використані повторно, і тому ви можете один раз підключити його до DI-контейнера і використовувати той самий, що використовується скрізь, тоді як MockupFactoryдля певного класу навряд використовувати більше місць - це означає, що потрібно більше конфігурації в порівнянні з добре відокремленими інтерфейсами.
Dathan
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.