DateTime.Now i podróż w czasie

DateTime.Now?

Tytułową właściwość zna każdy. Służy ona do pobrania aktualnej daty lokalnej. Jest jedną z najczęściej używanych właściwości systemowych, a zarazem jedną z bardziej problematycznych. Część z Was zapewne powie, że nie powinno się jej używać i lepiej użyć DateTime.UtcNow, aby nie mieć problemów, gdy mamy użytkowników w różnych strefach czasowych. Ale o tym kiedy indziej, dzisiaj chciałbym się skupić na testowaniu kodu, który potrzebuje informacji o aktualnym czasie (DateTime.Now).

Testowanie

Testowanie kodu korzystającego z DateTime.Now (lub DateTime.UtcNow) jest problematyczne, szczególnie w przypadku testów jednostkowych. Bo jak efektywnie przetestować kod:

Lub taki kod:

Założeniem testów jednostkowych jest przede wszystkim to, że wykonanie testu jest powtarzalne i przewidywalne. To w przypadku DateTime.Now (DateTime.UtcNow) jest niestety złamane, ponieważ za każdym razem otrzymujemy inną wartość. W szczególności drugi przykład spowoduje, że w pewnym momencie testy przestaną działać, bo osiągniemy określoną datę.

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 20 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Rozwiązanie?

Dobre testy jednostkowe powinny testować klasę w odizolowaniu od innych elementów aplikacji, w tym od elementów systemowych, takich jak DateTime.Now (DateTime.UtcNow). Z pomocą przychodzi nam wrapper w postaci na przykład interfejsu IDateService, który opakuje nam wywołanie DateTime.Now (DateTime.UtcNow). Wrapper będzie wstrzykiwany w formie zależności (na przykład przez konstruktor), dzięki czemu na poziomie testów jednostkowych będziemy mogli utworzyć atrapę z określonym czasem.

IDateService mógłby wyglądać następująco:

Natomiast sama implementacja tak:

Dzięki temu dla pierwszej naszej metody możemy napisać taki test:

A zmieniona implementacja klasy logiki biznesowej będzie wyglądała tak:

Podróż w czasie?

A co z tytułową podróżą w czasie? Kilka razy zdarzyło mi się tworzyć system, w którym aktualny czas był jednym z kluczowych elementów aplikacji. Na przykład był to portal z reklamami, w którym reklama po określonym czasie musiała przestać się pokazywać. Niestety testowanie takiej aplikacji przez testera jest dość problematyczne, w momencie gdy korzystamy z aplikacji z DateTime.Now (DateTime.UtcNow). Na szczęście z pomocą ponownie przychodzi nam DateService.

W swojej implementacji dodaję możliwość przesuwania się w czasie. DateService ma dwie dodatkowe metody. Pierwszą z nich jest SetNow, która otrzymuje czas określający nowy czas, od którego trzeba liczyć aktualny czas. Przykładowo, gdy tester dodał nowe ogłoszenie na 30 dni, może wykorzystać SetNow, aby przenieść się w czasie o te 30 dni. Dzięki temu, że cała aplikacja korzysta z DateService, worker, który testowo odpala się co 5 minut, przy kolejnym działaniu pobierze przyszłą datę i wyłączy wyświetlenie ogłoszenia. Dzięki temu tester może przetestować wygaszenie reklam w ciągu kilku minut, bez konieczności czekania, aż określony czas rzeczywiście minie.

Druga metoda usługi to metoda Reset, która przywraca aktualny czas. Właściwa implementacja DateService to:

Jak widać, użyłem typu TimeSpan w formie prywatnego pola, który zawiera różnicę między aktualnym czasem a czasem ustawionym przez użytkownika. Oczywiście aby wszystko działało poprawnie, nasza usługa musi być rejestrowana w kontenerze dependency injection jako singleton.

Chciałbym zaznaczyć jeszcze, że sam interfejs dla usługi (IDateService) nie zawiera dodatkowych metod, które są specyficzne tylko dla tej konkretnej implementacji.

Testowanie

W swoich aplikacjach ASP.NET MVC mam specjalny kontroler (DebugController), który służy do konfigurowania aplikacji na potrzeby testów. Kontroler ten nie jest dostępny w środowisku produkcyjnym.

W naszym przykładzie DebugController posiada akcje, które umożliwiają zmianę aktualnego czasu w systemie na podstawie tego, co użytkownik wprowadzi. Sam kontroler może również zawierać inne funkcjonalności przydatne przy testowaniu – na przykład widok, który wymusi uruchomienie workera wyłączającego reklamy, których data minęła.

Oczywiście ze zmianą czasu warto uważać w przypadku, gdy z jednej aplikacji testowej korzysta wielu testerów, ponieważ może się okazać, że poszczególni testerzy będą sobie nawzajem zmieniać czas. U nas w projekcie w takiej sytuacji każdy z testerów miał własną instancję aplikacji, w której mógł sobie dowolnie zmieniać czas.

Na githubie jest dostępny projekt, w którym możecie przetestować działanie DateService. Szczególnie zachęcam do zajrzenia do kontrolera DebugController. Aby przetestować aplikację, wystarczy wejść na stronę /Debug/Date, na której jest wyświetlany aktualny czas pobierany z serwera za pomocą ajax co jedną sekundę. Dodatkowo widok umożliwia również ustawienie nowej daty.

A jak Wy radzicie sobie w takich sytuacjach?

1 thought on “DateTime.Now i podróż w czasie

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *