
Po co mi interfejs IDateTimeProvider w konstruktorze?
Ostatnio spotkałem się z ciekawą opinią na temat obsługi daty w projekcie. Większość osób, które mają jakiekolwiek doświadczenie z testami jedntostowymi, wie, że pobieranie czasu powinno zostać opakowane w klasę, która implementuje interfejs który potem jest wykorzystywany przy wstrzykiwaniu zależności. Celem tego zabiegu jest to, aby przy testowaniu móc potem taki interfejs zamockować. Klasy korzystające z dat nie są bezpośrednio zależne od klasy DateTime.
Jednakże jest tu pewien problem. wyobraźmy sobie teraz, że wiele klas w naszym systemie korzysta z przysłoniętej pięknym interfejsem klasy DateTime. Każda klasa w konstruktorze musi zadeklarować, że jako parametr przyjmuje implementację interfejsu dostarczającego czas.
Czy naprawdę musimy wpychać ten interfejs do każdego konstruktora klasy, którą wykorzystujemy?
Istnieje ciekawe rozwiązanie problemu, który przedstawiłem wcześniej. Oczywiście obowiązkowym wymogiem jest zachowanie oryginalnych możliwości zamockowania pobieranej daty przy jednoczesnym usunięciu parametru z konstruktorów klas naszego systemu.
public static class DateTimeProviderContext { private static readonly AsyncLocal<DateTime?> UtcNowLocal = new AsyncLocal<DateTime?>(); public static DateTime? UtcNow => UtcNowLocal.Value; public static void Set(DateTime dateTime) { UtcNowLocal.Value = dateTime; } public static void Reset() { UtcNowLocal.Value = null; } } public static class DateTimeProvider { public static DateTime UtcNow => DateTimeProviderContext.UtcNow ?? DateTime.UtcNow; }
Powyższe dwie statyczne klasy spełniają wszystkie wymogi jakich potrzebujemy.
W naszym kodzie wykorzystujemy DateTimeProvider. Dzięki temu, że jest statyczny nie musimy tworzyć jego instancji. Ponadto nie musimy wstrzykiwać interfejsu dostarczającego datę w konstruktorach naszych klas.
A testowanie? Tutaj mamy DateTimeContext. W momencie. gdy potrzebujemy zamockować pobieraną datę wykorzystujemy metodę Set klasy DateTimeContext.
Chciałbym zwrócić uwagę na wykorzystanie AsyncLocal. Opis z dokumentacji brzmi:
Represents ambient data that is local to a given asynchronous control flow, such as an asynchronous method.
Dzięki temu wartość daty w DateTimeContext „nie wycieknie” do innego wątku jeśli np. nasze testy będą wywoływane równolegle na kilku wątkach (lub na tym samym wątku jeśli przy korzystaniu z async/await przydzielony zostanie ten sam wątek z puli – dlatego też nie jest wykorzystany tutaj ThreadLocal).