Pierwszy raz próbując napisać test jednostkowy dla metody wykorzystującej strukturę DateTime napotykamy na problem z odczytem aktualnego czasu z statycznych właściwości (UtcNow, Now, Today) struktury DateTime.  Jeśli logika testowanej metody zależy od aktualnego czasu lub daty, wynik testu będzie się różnić w zależności od czasu przeprowadzenia testu. Jak zarządzać aktualnym czasem w testach jednostkowych? Problem był, jest i będzie poruszany w sieci, poniżej prezentuję możliwe rozwiązania problemu.

Wrapper DateTime

W powyższym rozwiązaniu definiuję się interfejs dla statycznych właściwości i tworzy się dedykowaną implementacje interfejsu IDataTimeProvider. Następnie wstrzykuje się powyższą implementację interfejsu do klasy i podmienia się wywołania DateTime na DataTimeProvider.

public interface IDateTimeProvider
{
    DateTime Now { get; }
    DateTime UtcNow { get; }
    DateTime Today { get; }
}

public class DateTimeProvider : IDateTimeProvider
{
    public DateTime Now => DateTime.Now;

    public DateTime UtcNow => DateTime.UtcNow;

    public DateTime Today => DateTime.Today;
}

W ramach testu jednostkowego wstrzykuje się mocka dla interfejsu IDataTimeProvider do testowanej klasy. Problem rozwiązany, jednak od razu rzuca się w oczy, że powiększamy klasę o kolejną zależność, którą trzeba wstrzyknąć np. przez konstruktor.

Static Provider

Kolejną opcją jest implementacja statycznej klasy, która będzie umożliwiała podmianę logiki obsługi aktualnego czasu na potrzebę testów jednostkowych.

public static class DateTimeProvider
{
    public static Func<DateTime> Now = () => DateTime.Now;

    public static Func<DateTime> UtcNow = () => DateTime.UtcNow;

    public static Func<DateTime> Today = () => DateTime.Today;
}

Powyższe rozwiązanie można rozbudować dodając metodę do ustawiania i przywracania domyślnej logiki związanej z obsługą czasu.

public static class DateTimeProvider
{
    private static Func<DateTime> now = () => DateTime.Now;
    private static Func<DateTime> utcNow = () => DateTime.UtcNow;
    private static Func<DateTime> today = () => DateTime.Today;

    public static DateTime Now => now();
    public static DateTime UtcNow => utcNow();
    public static DateTime Today => today();

    public static void SetNowLogic(Func<DateTime> logic) => now = logic;
    public static void SetUtcNowLogic(Func<DateTime> logic) => utcNow = logic;
    public static void SetTodayLogic(Func<DateTime> logic) => today = logic;

    public static void RestoreNowLogic() => now = () => DateTime.Now;
    public static void RestoreUtcNowLogic() => utcNow = () => DateTime.UtcNow;
    public static void RestoreTodayLogic() => today = () => DateTime.Today;
}

Logika klasy DataTimeProvider rozbudowała się o metody, które tylko będą wykorzystywane do testów jednostkowych. Osobiście wolę wersje bez tej dodatkowej logiki. W obu przypadkach podmieniamy metodę w delegacie na potrzeby wykonania scenariusza testowego.

Ambient Context

Ostatnim rozwiązaniem w celu opanowania czasu jest wykorzystanie abstrakcyjnej klasy DateTimeProvider, którą opisuje w swojej książce Dependency Injection in .NET Mark Seemann. Klasa DateTimeProvider zwraca aktualnie przypisaną instancje do właściwości Current, domyślnie jest to instancja DefaultDateTimeProvider. Za każdym razem gdy odwołujemy się do właściwości np. UtcNow zwracana jest wartość utworzona przez aktualną instancje DateTimeProvider.

public abstract class DateTimeProvider
{
    private static DateTimeProvider current;

    static DateTimeProvider()
    {
        current = new DefaultDateTimeProvider();
    }

    public static DateTimeProvider Current
    {
        get { return current; }
        set
        {
            if (value == null)
            {
                throw new ArgumentNullException(nameof(value));
            }
            current = value;
        }
    }

    public abstract DateTime Now { get; }

    public abstract DateTime UtcNow { get; }

    public abstract DateTime Today { get; }

    public static void ResetToDefault()
    {
        current = new DefaultDateTimeProvider();
    }
}

public class DefaultDateTimeProvider : DateTimeProvider
{
    public override DateTime Now => DateTime.Now;

    public override DateTime UtcNow => DateTime.UtcNow;

    public override DateTime Today => DateTime.Today;
}

W implementacji DefaultDateTimeProvider przesłonięte są właściwości odnoszące się do aktualnego czasu. Przykład użycia DateTimeProvider można zobaczyć poniżej.

var startTime = DateTimeProvider.Current.UtcNow;

Na potrzeby testów jednostkowych do właściwości Current przypisujemy mocka, poniżej przykład z wykorzystaniem NSubstitute.

var mockDateTimeProvider = Substitute.For<DateTimeProvider>();
mockDateTimeProvider.Now.Returns(new DateTime(2018, 02, 12));
DateTimeProvider.Current = mockDateTimeProvider;

Podsumowanie

W przedstawionych przykładach założono, że nie ma kodu wielowątkowego i nikogo nie korci podmiana logiki biznesowej obsługi czasu w kodzie produkcyjnym. Wszystkie powyższe podejścia umożliwiają sterowanie aktualnym czasem na potrzeby testów jednostkowych. A jaki sposób jest u was wykorzystywany ?