Kurs TDD cz. 20: Mockowanie DateTime.Now, random, static, itp.

Jedną z największych trudności dla osoby zaczynającej przygodę z testami jednostkowymi są:

  • Metody i klasy statyczne.
  • Niederministyczne lub/i niepowtarzalne zależności.
    • Testy jednostkowe muszą być deterministyczne i powtarzalne.
    • Musimy przyjąć zatem strategię wstrzykiwania alternatywnej implementacji dla wywołań DateTime.Now, funkcji losującej, itp.

W tym artykule przedstawię jedną ze strategii tworzenia atrap dla tego typu zależności.

Co będziemy testować?

Przyjmijmy, że chcemy przetestować metodę GetAge klasy AgeCalculator która, jak sama nazwa wskazuje, zwraca wiek danej osoby. Przykładowa implementacja (źródło) wygląda następująco:

public class AgeCalculator
{
    public int GetAge(DateTime dateOfBirth)
    {
        DateTime now = DateTime.Now;
        int age = now.Year - dateOfBirth.Year;

        if (now.Month < dateOfBirth.Month || (now.Month == dateOfBirth.Month && now.Day < dateOfBirth.Day))
        {
            age--;
        }

        return age;
    }
}

Oczywiście, nie jest to algorytm idealny i sam nie użyłbym go u siebie ze względu na brak wsparcia dla:

Algorytm jest jednak prosty i spełnia nasze założenia, tj. wywołuje metodę DateTime.Now, która nie jest powtarzalna.

Wzorzec Provider

Jednym z najprostszych rozwiązań jest oddelegowanie kontroli nad daną funkcjonalnością do osobnej klasy. W naszym przypadku będzie to oddelegowanie wywołania DateTime.Now:

public interface IDateTimeProvider
{
    DateTime GetDateTime();
}

public class DateTimeProvider : IDateTimeProvider
{
    public DateTime GetDateTime() => DateTime.Now;
}

Zmieniony kalkulator wykorzystujący providera wygląda następująco:

public class AgeCalculator
{
    private readonly IDateTimeProvider _dateTimeProvider;

    public AgeCalculator(IDateTimeProvider dateTimeProvider)
    {
        if (dateTimeProvider == null) throw new ArgumentNullException(nameof(dateTimeProvider));
        _dateTimeProvider = dateTimeProvider;
    }

    public int GetAge(DateTime dateOfBirth)
    {
        DateTime now = _dateTimeProvider.GetDateTime();
        // ...
    }
}

Strategia ta pozwala na podmianę implementacji providera na testowy:

[Test]
public void Test()
{
    var currentDate = new DateTime(2015, 1, 1);
    var dateTimeProvider = Mock.Of<IDateTimeProvider>(provider => provider.GetDateTime() == currentDate);

    var ageCalculator = new AgeCalculator(dateTimeProvider);

    var dateOfBirth = new DateTime(1990, 1, 1);
    int age = ageCalculator.GetAge(dateOfBirth);

    age.Should().Be(25);
}

Podczas testu domyślna strategia pobierania daty zostaje podmieniona na testową, której wartość można dowolnie dostosowywać do założeń naszego testu.

Alternatywnie, można stworzyć provider typu generycznego, czyli IProvider<T>.

W taki sam sposób możemy opakować (ang. wrap) wywołania klas lub/i metod statycznych. Lepszy sufiks dla takiego wzorca będzie “Wrapper”.

Pytania otwarte (a niektóre zamknięte)

Na deser zostawiam kilka pytań czytelnikowi:

  • Co, oprócz testowalności, zyskujemy dzięki powyższej strategii?
  • Czy nie lepiej pozostawić logikę biznesową niezmienioną, a w teście modyfikować daty w zależności od DateTime.Now?
  • Czy nie lepiej przekazać DateTime.Now jako parametr metody?
  • Czy nie lepiej przekazać delegat lub Func<DateTime> jako parametr metody lub w konstruktorze klasy?
  • Czy nie lepiej stworzyć singleton lub globalną klasę statyczną, która posiada domyślną implementację, którą można podmienić?
  • Czy w naszym przypadku mamy do czynienia z potencjalnym race condition?

Kod źródłowy

Przypominam, że kod źródłowy całego kursu TDD, jak i tego rozdziału jest dostępny na GitHubie: https://github.com/dariusz-wozniak/TddCourse.

Źródła

5 thoughts on “Kurs TDD cz. 20: Mockowanie DateTime.Now, random, static, itp.

  1. Pingback: dotnetomaniak.pl
  2. Co, oprócz testowalności, zyskujemy dzięki powyższej strategii?
    Nadmiar kodu z pewnością :) Sam używam powyższej strategii przy pracy naokoło warstwy sieciowej. Ważne by znaleźć umiar i nie tworzyć interfejsu do wszystkiego…
    Czy nie lepiej pozostawić logikę biznesową niezmienioną, a w teście modyfikować daty w zależności od DateTime.Now?
    Wymaga to gimnastyki przy asercjach… Tutaj wstrzyknięcie interfejsu bardzo łatwo obrazuje co jest testowane i w jaki sposób, dlatego skłaniam się przy aktualnym rozwiązaniu.
    Czy nie lepiej przekazać DateTime.Now jako parametr metody? AgeCalculator wygląda na komponent który sam powinien decydować o tym jak oblicza wiek. Przekazywanie tej wartości jako argument jest trochę niebezpieczne pod kątem potencjalnych błędów
    Czy nie lepiej przekazać delegat lub Func jako parametr metody lub w konstruktorze klasy?
    Bardzo lubię ten styl testowania. Internalowy konstructor przyjmujący Funca, InternalsVisibleTo i jedziemy! ;)
    Czy nie lepiej stworzyć singleton lub globalną klasę statyczną, która posiada domyślną implementację, którą można podmienić?
    Singletony/statyki, cache to rzeczy których unikam jak ognia. Tworzą one idealne miejsce na późniejsze problemy w testowanu, zależnościach między testami…
    Czy w naszym przypadku mamy do czynienia z potencjalnym race condition?
    Provider jest narażony na równolegle wykorzystanie. Ma on jedną metodę, która zwraca Datetime.Now, więc potencjalny wyścig wątków nie jest niebezpieczny.

    • Zgadzam się z Tobą.

      Wstrzykiwanie Func’ów i delegatów jest bardzo ciekawą strategią, jednak należy pamiętać o Do Not Repeat Yourself, zwłaszcza w kontekście definiowania domyślnych zachowań.

      Jeśli chodzi o singleton lub/i globalną klasę statyczną, warto zaznajomić się z rozwiązaniem Ambient Context http://stackoverflow.com/a/2425739/297823 lub rozwiązaniem Ayende i Procenta (link w komentarzu Radka Maziarki).

    • Użycie takiego statycznego providera i wzorca Ambient Context jest ciekawą alternatywą, zwłaszcza w kontekście tak uniwersalnego problemu jak wyciąganie aktualnego czasu. Pozostają dwie kwestie:

      1. Opisałem tutaj raczej generyczne rozwiązanie dla przypadków, w których musimy wstrzykiwać zależność dane zachowanie ze względu na losowość, niedeterministyczność lub ograniczenia techniczne (static). DateTime.Now jest przypadkiem szczególnym dla którego Ambient Context mógłby być poprawnym rozwiązaniem, jednak dla pozostałych wstrzykiwanie zależności przez konstruktor lub właściwość jest bardziej sensowne.

      2. Ambient Context ma kilka poważnych wad, o czym pisze Mark Seemann w “Dependency Injection in .NET”, m.in. “implicitness” (nie możemy określić czy wykorzystujemy klasę Ambient Context patrząc na interfejs) i kłopoty z poprawną implementacją (wielowątkowość i pułapki z NullReferenceException).

      Ambient Context jest dobrą alternatywną dla obsługi globalnej aktualnego czasu, jednak w tym przypadku głosowałbym za wstrzyknięciem zależności. W przypadku daty aktualnej posiadamy domyślne zachowanie i z tego powodu zmieniłbym wstrzykiwanie przez konstruktor na wstrzykiwanie przez właściwość.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s