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:

public User Update(User user)
{
user.UpdatedDate = DateTime.Now;
//zapis użytkownika
return user;
}
view raw UserLogic1.cs hosted with ❤ by GitHub

Lub taki kod:

public User Create(string name)
{
if (DateTime.Now < new DateTime(2017, 9, 1))
{
throw new Exception("Rejestracja użytkowników jest zamknięta do 1 września 2017.");
}
User user = new User();
user.Name = name;
//dodanie użytkownika
return user;
}
view raw UserLogic2.cs hosted with ❤ by GitHub

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 30 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:

public interface IDateService
{
DateTime Now { get; }
DateTime UtcNow { get; }
}

Natomiast sama implementacja tak:

public class DateService : IDateService
{
public DateTime Now
{
get
{
return DateTime.Now;
}
}
public DateTime UtcNow
{
get
{
return DateTime.UtcNow;
}
}
}

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

[TestFixture]
public class UserLogicTests
{
private Mock<IDateService> _dateServiceMock;
private UserLogic Create()
{
_dateServiceMock = new Mock<IDateService>();
return new UserLogic(_dateServiceMock.Object);
}
[Test]
public void Update_SetUpdatedDate()
{
var userLogic = Create();
_dateServiceMock.Setup(u => u.Now)
.Returns(new DateTime(2017, 1, 1));
var user = userLogic.Update(new User());
Assert.AreEqual(new DateTime(2017, 1, 1), user.UpdatedDate);
}
}

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

public class UserLogic
{
private IDateService _dateService;
public UserLogic(IDateService dateService)
{
_dateService = dateService;
}
public User Update(User user)
{
user.UpdatedDate = _dateService.Now;
//zapis użytkownika
return user;
}
}
view raw UserLogic3.cs hosted with ❤ by GitHub

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:

public class DateService : IDateService
{
private TimeSpan _offset = new TimeSpan();
public DateTime Now
{
get
{
return DateTime.Now + _offset;
}
}
public DateTime UtcNow
{
get
{
return DateTime.UtcNow + _offset;
}
}
public void SetNow(DateTime date)
{
_offset = date - DateTime.Now;
}
public void Reset()
{
_offset = new TimeSpan();
}
}

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 e-mail nie zostanie opublikowany.