Час – це слизька доріжка в автоматизації в цілому, і особливо в .NET. Але взаємодії з часом присутні всюди: планування завдань, логування подій, емуляція затримок в робочих процесах, перевищення строку давності.
TimeProvider та ITimer у .NET 8: як спростити юніт-тестування залежностей від часу
Стандартні методи взаємодії з часом, такі як:
- DateTime.Now
- DateTime.UtcNow
- Thread.Sleep
- Task.Delay
- System.Threading.Timer
Не є ідеальними, бо вони породжують багато непотрібних нам складностей:
- Складності в тестуванні: прямі виклики до реальної системи – важко симулювати реальні часові умови без стандартних методів очікування (умовний Thread.Sleep).
- Код стає дуже залежним від статичного часу і не може бути легко зміненим.
- Складно замокати статичні методи DateTime.UtcNow.
- Навіть таймери нижчого рівня потребують ручного видалення, обробки умов перегонів і синхронізації логіки.
- Ускладнює роботу на пайплайнах і віддалених середовищах – різниця в часі таки має значення.
Коли в код вбудовується DateTime.Now або DateTime.UtcNow, це робить код разом з юніт-тестами залежними від зовнішнього світу. Це порушує основні принципи SOLID.
Приклад проблеми з DateTime.Now у юніт-тестах:
Розглянемо простий сервіс, який вітає користувачів залежно від часу доби:
ПРИЄДНУЙСЯ ДО НАШОЇ КОМАНДИ
Тестування цього методу є проблематичним. Щоб перевірити привітання «Good morning!», вам потрібно запустити тест до полудня. Щоб протестувати «Good evening!», тест має виконуватися ввечері. Це робить ваші тести залежними від часу їх виконання, що призводить до недетермінованих результатів.
Щоб обійти це, розробники часто вдавалися до створення власних абстракцій часу, таких як інтерфейс ISystemClock, який потім можна було б імітувати (мокати) в тестах. Хоча цей підхід працює, він додає шаблонний код і не має стандартизованої реалізації в різних проєктах і командах.
Серед проблем також буде повільність та недетермінованість – результат таким чином буде залежати від навантаження та планувальника ОС.
Але саме для цього існує TimeProvider та ITimer.
Що це взагалі за звір?
TimeProvider — це нова абстракція в .NET для роботи з часом і оскільки це абстракція, його можна легко замінити «фейковою» реалізацією під час тестування.
Ось деякі з ключових особливостей TimeProvider:
- абстракція часу: він надає віртуальні методи, такі як GetUtcNow() та GetLocalNow(), для отримання поточного часу у вигляді DateTimeOffset;
- Dependency Injection: TimeProvider розроблений для використання з Dependency Injection, що дозволяє вам надавати різні реалізації для продакшн-середовища та для тестування;
- тестованість: NuGet-пакет Microsoft.Extensions.TimeProvider.Testing містить клас FakeTimeProvider, який надає вам повний контроль над часом у ваших тестах.
Звісно, цей TimeProvider вийшов трошки завеликий, маючи дуже багато методів всередині, але за допомогою них спробуємо тепер зарефакторити наш метод з першого прикладу:
В Program.cs або в конфігах запуску – додаємо:
Тепер нам якось треба протестувати наш «вітальник». А отже час викликати FakeTimeProvider, щоб подивитись, як він перетворює наші юніт-тести. Спочатку вам потрібно додати пакет Microsoft.Extensions.TimeProvider.Testing до вашого тестового проєкту.
З FakeTimeProvider ми тепер можемо «заморозити» час у будь-який бажаний момент, роблячи наші тести послідовними та надійними, незалежно від того, коли вони запускаються. FakeTimeProvider також дозволяє симулювати плин часу за допомогою методу Advance(), що є неоціненним для тестування логіки, яка охоплює певний проміжок часу.
А як щодо підкорення Timer?
Тестування операцій, що запускаються таймерами, як-от ті, що використовують System.Timers.Timer або System.Threading.Timer, створює ще один набір проблем. Але тут виникають також проблеми, бо потрібно або реально чекати (Thread.Sleep), що сповільнює тести, або взагалі неможливо протестувати логіку без складних обгорток.
Але у TimeProvider є метод боротьби зі звичними методами таймерів - CreateTimer, який повертає екземпляр ITimer. Це, в поєднанні з FakeTimeProvider, дозволяє негайно виконувати колбеки таймера контрольованим чином.
Порівняємо шляхи розв'язання проблеми з традиційним таймером та використовуючи TimeProvider.
Створимо сервіс, який періодично виконує фонове завдання:
Тестування цього вимагало б очікування понад хвилину, що є вкрай непрактичним для набору юніт-тестів.
А тепер переробимо це з використанням TimeProvider та ITimer:
Тепер ми можемо написати відповідний юніт-тест, який вже не буде чекати:
Викликаючи fakeTimeProvider.Advance(), ми вручну переміщуємо час уперед, змушуючи будь-які заплановані таймери спрацьовувати негайно. Це усуває потребу в Thread.Sleep і робить тест миттєвим та детермінованим.
Просунуті сценарії та міркування
Але за допомогою TimeProvider та ITimer можна імплементувати й складніші сценарії:
- Часові пояси: FakeTimeProvider дозволяє встановити конкретний часовий пояс за допомогою SetLocalTimeZone(), що є критично важливим для тестування додатків, які поводяться по-різному залежно від місцеперебування користувача.
- Асинхронні операції: Task.Delay та Task.WaitAsync тепер мають перевантаження, які приймають TimeProvider, що дозволяє вам контролювати їхню поведінку в тестах.
- Інтеграційне тестування: хоча ці абстракції ідеальні для юніт-тестів, вони також можуть бути корисними в інтеграційних тестах, де потрібно контролювати чутливі до часу взаємодії між різними частинами вашої системи.
- Стрес-тести розкладів: перевірка логіки з інтервалами (наприклад, кожні 5 хвилин) можлива у долі секунди тестового часу.
- Високоточні заміри продуктивності: GetTimestamp() дає монотонний відлік без впливу зміни системного часу.
- Єдиний час між сервісами: в мікросервісах можливо впроваджувати той самий інстанс TimeProvider у всі сервіси – це підвищує стабільність інтеграційних тестів.
Висновок: зміна парадигми в тестуванні .NET
Отже, TimeProvider та ITimer у .NET 8 нарешті розв'язують давню проблему тестування часозалежного коду. Завдяки цим абстракціям розробники можуть значно спростити процес написання надійних юніт-тестів та позбутися нестабільних «flaky tests».
Впровадження залежності від часу через TimeProvider дозволяє легко контролювати час у тестовому середовищі, що робить тести передбачуваними та стабільними.
Підсумовуючи основні переваги нових інструментів:
- можливість впровадження залежності від часу через конструктор;
- повний контроль над плином часу за допомогою FakeTimeProvider;
- детерміновані тести без необхідності реального очікування;
- уникнення race condition при використанні ITimer.
Безсумнівно, ці нововведення мають особливу цінність у CI/CD середовищах, де стабільність тестів критично важлива. Тепер замість створення власних абстракцій часу або використання сторонніх бібліотек, розробники отримали офіційне, добре продумане рішення безпосередньо від Microsoft.
Насправді з появою TimeProvider та ITimer тестування коду, що залежить від часу та таймерів, стало настільки простим, що вже немає виправдань для впровадження DateTime.Now чи DateTime.UtcNow безпосередньо в бізнес-логіку. Відтепер, розробляючи нові компоненти або рефакторячи наявний код, варто зосередитися на правильному впровадженні залежностей від часу через ці абстракції.
Зрештою, TimeProvider і ITimer – це чудовий приклад того, як невеликі, але добре продумані зміни в платформі можуть суттєво покращити якість коду та спростити процес розробки. Тому настійно рекомендую почати використовувати ці інструменти вже сьогодні для створення більш надійних та легко тестованих додатків.
Підписатися на новини
-
Думка експертаOperational Intelligence - Tech Pulse | Дайджест #2
У цьому випуску ми розглядаємо кілька практичних нюансів OpenTelemetry, проблему з якістю даних, оновлення від провайдерів і хто відповідає за які частини observability-стеку.
-
Думка експертаЦифрові двійники в IT: ключові архітектурні патерни та рішення
-
Думка експертаПеревірка етичності AI у фінтехі
-
Лайфхаки
Що таке Operational Intelligence в EPAM і навіщо вам читати Tech Pulse
-
Думка експертаAI в музиці: коли голос стає продуктом
Чому тема «AI в музиці» — це не про заміщення музикантів, а про нові правила гри на ринку, де виробництво контенту тепер практично безкоштовне.