Udostępnij za pośrednictwem


Najlepsze rozwiązania dotyczące testowania jednostkowego dla platformy .NET

Istnieje wiele zalet pisania testów jednostkowych. Pomagają one w regresji, udostępniają dokumentację i ułatwiają dobre projektowanie. Ale gdy testy jednostkowe są trudne do odczytania i kruche, mogą siać spustoszenie w kodzie źródłowym. W tym artykule opisano niektóre najlepsze rozwiązania dotyczące projektowania testów jednostkowych w celu obsługi projektów .NET Core i .NET Standard. Nauczysz się technik, aby utrzymać testy odporne i łatwe do zrozumienia.

Przez John Reese ze szczególnymi podziękowaniami dla Roy Osherove

Zalety testowania jednostkowego

W poniższych sekcjach opisano kilka powodów pisania testów jednostkowych dla projektów .NET Core i .NET Standard.

Krótszy czas wykonywania testów funkcjonalnych

Testy funkcjonalne są kosztowne. Zazwyczaj obejmują one otwarcie aplikacji i wykonanie szeregu kroków, które użytkownik (lub ktoś inny) musi wykonać, aby zweryfikować oczekiwane zachowanie. Te kroki mogą nie zawsze być znane testerowi. Muszą skontaktować się z kimś bardziej kompetentnym w okolicy, aby przeprowadzić test. Testowanie może potrwać kilka sekund w przypadku trywialnych zmian lub kilka minut w przypadku większych zmian. Na koniec ten proces musi być powtarzany dla każdej zmiany, która jest wprowadzana w systemie. Testy jednostkowe, z drugiej strony, zajmują milisekundy, mogą być uruchamiane za pomocą jednego przycisku i niekoniecznie wymagają żadnej wiedzy na temat całego systemu. Moduł uruchamiający testy określa, czy test przebiegnie pomyślnie, czy kończy się niepowodzeniem, a nie osobą.

Ochrona przed regresją

Wady regresji to błędy wprowadzane po wprowadzeniu zmiany w aplikacji. Testerzy często testują nie tylko swoją nową funkcję, ale także funkcje testowe, które istniały wcześniej, aby sprawdzić, czy istniejące funkcje nadal działają zgodnie z oczekiwaniami. Dzięki testom jednostkowym można ponownie uruchomić cały zestaw testów po każdej kompilacji, a nawet po zmianie wiersza kodu. Takie podejście pomaga zwiększyć pewność, że nowy kod nie przerywa istniejącej funkcjonalności.

Wykonywalna dokumentacja

Nie zawsze może być oczywiste, co robi dana metoda lub jak zachowuje się, biorąc pod uwagę określone dane wejściowe. Możesz zadać sobie pytanie: Jak działa ta metoda, jeśli przekażę mu pusty ciąg lub wartość null? Jeśli masz zestaw dobrze nazwanych testów jednostkowych, każdy test powinien jasno wyjaśnić oczekiwane wyniki dla danego wejścia. Ponadto test powinien być w stanie sprawdzić, czy faktycznie działa.

Mniej powiązany kod

Gdy kod jest ściśle powiązany, może być trudny do testowania jednostkowego. Bez tworzenia testów jednostkowych dla kodu, który piszesz, poziom sprzężenia może być mniej widoczny. Pisanie testów dla kodu w naturalny sposób rozdziela kod, ponieważ jest to trudniejsze do przetestowania w przeciwnym razie.

Cechy dobrych testów jednostkowych

Istnieje kilka ważnych cech, które definiują dobry test jednostkowy:

  • Fast: Nie jest rzadkością w dojrzałych projektach mieć tysiące testów jednostkowych. Uruchomienie testów jednostkowych powinno zająć trochę czasu. Milisekund.
  • Izolowane: Testy jednostkowe są niezależne, mogą uruchamiać się w izolacji i nie mają zależności od czynników zewnętrznych, takich jak system plików lub baza danych.
  • Powtarzalne: Uruchomienie testu jednostkowego powinno dawać spójne wyniki. Test zawsze zwraca ten sam wynik, jeśli nie zmieniasz niczego między przebiegami.
  • Autotestowanie: test powinien automatycznie wykrywać, czy przeszedł lub nie bez żadnej interakcji z człowiekiem.
  • timely: Test jednostkowy nie powinien trwać nieproporcjonalnie długo w porównaniu z testowanym kodem. Jeśli okaże się, że testowanie kodu zajmuje dużo czasu w porównaniu z pisaniem kodu, rozważ bardziej testowy projekt.

Pokrycie kodu i jakość kodu

Wysoki procent pokrycia kodu jest często skojarzony z wyższą jakością kodu. Jednak sam pomiar nie może określić jakości kodu. Ustawienie zbyt ambitnego celu procentowego pokrycia kodu może być nieproduktywne. Rozważ złożony projekt z tysiącami gałęzi warunkowych i załóżmy, że postawiłeś sobie cel 95% pokrycia kodu dla%. Obecnie projekt utrzymuje pokrycie kodu na poziomie 90%%. Czas potrzebny na rozpatrzenie wszystkich przypadków brzegowych w pozostałych 5% może wymagać ogromnego nakładu pracy, a korzyści z tego szybko się zmniejszają.

Procent wysokiego pokrycia kodu nie jest wskaźnikiem sukcesu i nie oznacza wysokiej jakości kodu. Odnosi się to tylko do zakresu kodu objętego testami jednostkowymi. Aby uzyskać więcej informacji, zobacz pokrycie kodu testów jednostkowych.

Terminologia dotycząca testowania jednostkowego

Kilka terminów jest często używanych w kontekście testów jednostkowych: fałszywych, pozorowaći . Niestety, te terminy można źle zastosować, dlatego ważne jest, aby zrozumieć prawidłowe użycie.

  • Fake: Fałszywy to ogólny termin, który może służyć do opisania wycinku lub pozornego obiektu. To, czy obiekt jest szkieletem, czy mockiem, zależy od kontekstu, w którym jest używany. Innymi słowy, fałszywe może być stub lub makiety.

  • Mock: pozorny obiekt jest fałszywym obiektem w systemie, który decyduje, czy test jednostkowy kończy się pomyślnie, czy nie. Makieta zaczyna się jako fałszywa i pozostaje fałszywa, dopóki nie wejdzie w operację Assert.

  • Stub: Stub to kontrolowane zastępstwo istniejącej zależności (lub współpracownika) w systemie. Korzystając z wycinku, możesz przetestować kod bez bezpośredniego radzenia sobie z zależnością. Domyślnie stub zaczyna się jako fałszywy.

Rozważ następujący kod:

var mockOrder = new MockOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Ten kod przedstawia wycinkę nazywaną makietą. Ale w tym scenariuszu stub jest naprawdę prototypem. Celem kodu jest przekazanie kolejności jako metody utworzenia wystąpienia obiektu Purchase (testowego systemu). Nazwa klasy MockOrder jest myląca, ponieważ kolejność jest wycinką, a nie pozorem.

Poniższy kod przedstawia dokładniejszy projekt:

var stubOrder = new FakeOrder();
var purchase = new Purchase(stubOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Gdy nazwa klasy zostanie zmieniona na FakeOrder, klasa jest bardziej ogólna. Klasa może być używana jako makiety lub wycinkę, zgodnie z wymaganiami przypadku testowego. W pierwszym przykładzie klasa FakeOrder jest używana jako wycink i nie jest używana podczas operacji Assert. Kod przekazuje klasę FakeOrder do klasy Purchase tylko w celu spełnienia wymagań konstruktora.

Aby użyć klasy jako makiety, możesz zaktualizować kod:

var mockOrder = new FakeOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(mockOrder.Validated);

W tym projekcie kod sprawdza właściwość na obiekcie zastępczym (twierdząc to), w związku z tym klasa mockOrder jest obiektem pozorowanym.

Ważne

Ważne jest, aby prawidłowo zaimplementować terminologię. Jeśli wywołasz wycinki "makiety", inni deweloperzy będą wprowadzać fałszywe założenia dotyczące twojej intencji.

Główna rzecz, którą należy pamiętać o zastępnikach w porównaniu ze ślepymi próbkami, to że zastępniki działają jak ślepe próbki, z wyjątkiem procesu Assert. Przeprowadzasz Assert operacji na obiekcie mock, ale nie na stubie.

Najlepsze rozwiązania

Istnieje kilka ważnych najlepszych rozwiązań, które należy stosować podczas pisania testów jednostkowych. W poniższych sekcjach przedstawiono przykłady pokazujące, jak zastosować najlepsze rozwiązania do kodu.

Unikanie zależności infrastrukturalnych

Spróbuj nie wprowadzać zależności od infrastruktury podczas pisania testów jednostkowych. Zależności sprawiają, że testy są powolne i kruche i powinny być zarezerwowane do testów integracji. Można uniknąć tych zależności w aplikacji, postępując zgodnie z jawną zasadą zależności i za pomocą iniekcji zależności platformy .NET. Testy jednostkowe można również zachować w osobnym projekcie niż testy integracji. Takie podejście zapewnia, że projekt testu jednostkowego nie zawiera odwołań do pakietów infrastruktury ani zależności.

Przestrzegaj standardów nazewnictwa testów

Nazwa testu powinna składać się z trzech części:

  • Nazwa testowanej metody
  • Scenariusz, w którym jest testowana metoda
  • Oczekiwane zachowanie podczas wywoływanego scenariusza

Standardy nazewnictwa są ważne, ponieważ pomagają wyrazić cel testu i aplikację. Testy to nie tylko upewnienie się, że kod działa. Udostępniają również dokumentację. Po prostu patrząc na zestaw testów jednostkowych, powinno być możliwe wnioskowanie zachowania kodu i nie trzeba patrzeć na sam kod. Co więcej, gdy testy kończą się niepowodzeniem, możesz zobaczyć dokładnie, które scenariusze nie spełniają Twoich oczekiwań.

oryginalny kod

[Fact]
public void Test_Single()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

Stosowanie najlepszych rozwiązań

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

Rozmieszczanie testów

Wzorzec "Przygotuj, Działaj, Potwierdź" jest typowym podejściem do pisania testów jednostkowych. Jak wskazuje nazwa, wzorzec składa się z trzech głównych zadań:

  • Rozmieść swoje obiekty, twórz i konfiguruj je w razie potrzeby
  • Działaj na obiekcie
  • Potwierdź, że coś jest zgodnie z oczekiwaniami

W przypadku przestrzegania wzorca można wyraźnie oddzielić testowane elementy od zadań Rozmieszczanie i Aserowanie. Wzorzec pomaga również zmniejszyć możliwość mieszania asercji z kodem w zadaniu Act.

Czytelność jest jednym z najważniejszych aspektów podczas pisania testu jednostkowego. Oddzielenie każdej akcji wzorca w teście wyraźnie wyróżnia zależności wymagane do wywołania kodu, sposobu wywoływania kodu i tego, co próbujesz potwierdzić. Chociaż może być możliwe połączenie niektórych kroków i zmniejszenie rozmiaru testu, ogólnym celem jest uczynienie testu tak czytelnym, jak to możliwe.

oryginalny kod

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Assert
    Assert.Equal(0, stringCalculator.Add(""));
}

Stosowanie najlepszych rozwiązań

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Act
    var actual = stringCalculator.Add("");

    // Assert
    Assert.Equal(0, actual);
}

Pisz testy spełniające minimalne kryteria zaliczenia

Dane wejściowe testu jednostkowego powinny być najprostszymi informacjami potrzebnymi do zweryfikowania aktualnie testowego zachowania. Podejście minimalistyczne pomaga testom stać się bardziej odporne na przyszłe zmiany w bazie kodu i skupić się na weryfikowaniu zachowania w implementacji.

Testy zawierające więcej informacji niż wymagane do przeprowadzenia bieżącego testu mają większe szanse na wprowadzenie błędów do testu i mogą sprawić, że intencja testu będzie mniej jasna. Podczas pisania testów chcesz skupić się na zachowaniu. Ustawianie dodatkowych właściwości w modelach lub używanie wartości niezerowych, gdy nie są potrzebne, tylko przeszkadza w tym, co próbujesz potwierdzić.

oryginalny kod

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("42");

    Assert.Equal(42, actual);
}

Stosowanie najlepszych rozwiązań

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

Unikaj ciągów magicznych

Ciągi magiczne to wartości ciągów znakowych zakodowane bezpośrednio w testach jednostkowych bez żadnych dodatkowych komentarzy ani kontekstu. Te wartości sprawiają, że kod jest mniej czytelny i trudniejszy do utrzymania. Ciągi magiczne mogą powodować zamieszanie dla czytelnika testów. Jeśli ciąg wygląda nietypowo, może się zastanawiać, dlaczego określona wartość została wybrana dla parametru lub wartości zwracanej. Ten typ wartości ciągu może prowadzić do bliższego przyjrzenia się szczegółom implementacji, a nie skupieniu się na teście.

Wskazówka

Uczyń swoim celem wyrażenie jak najwięcej intencji w kodzie testu jednostkowego. Zamiast używać ciągów magicznych, przypisz wszystkie trwale zakodowane wartości do stałych.

oryginalny kod

[Fact]
public void Add_BigNumber_ThrowsException()
{
    var stringCalculator = new StringCalculator();

    Action actual = () => stringCalculator.Add("1001");

    Assert.Throws<OverflowException>(actual);
}

Stosowanie najlepszych rozwiązań

[Fact]
void Add_MaximumSumResult_ThrowsOverflowException()
{
    var stringCalculator = new StringCalculator();
    const string MAXIMUM_RESULT = "1001";

    Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);

    Assert.Throws<OverflowException>(actual);
}

Unikanie logiki kodowania w testach jednostkowych

Podczas pisania testów jednostkowych należy unikać ręcznego łączenia ciągów, warunków logicznych, takich jak if, while, fori switchoraz inne warunki. Jeśli uwzględnisz logikę w zestawie testów, prawdopodobieństwo wprowadzenia usterek znacznie wzrośnie. Ostatnim miejscem, w którym chcesz znaleźć usterkę, jest pakiet testowy. Należy mieć wysoki poziom pewności, że testy działają, w przeciwnym razie nie można im ufać. Testy, którym nie ufasz, nie udostępniają żadnej wartości. Gdy test zakończy się niepowodzeniem, chcesz mieć poczucie, że coś jest nie tak z kodem i że nie można go zignorować.

Wskazówka

Jeśli dodanie logiki w teście wydaje się nieuniknione, rozważ podzielenie testu na co najmniej dwa różne testy, aby ograniczyć wymagania logiki.

oryginalny kod

[Fact]
public void Add_MultipleNumbers_ReturnsCorrectResults()
{
    var stringCalculator = new StringCalculator();
    var expected = 0;
    var testCases = new[]
    {
        "0,0,0",
        "0,1,2",
        "1,2,3"
    };

    foreach (var test in testCases)
    {
        Assert.Equal(expected, stringCalculator.Add(test));
        expected += 3;
    }
}

Stosowanie najlepszych rozwiązań

[Theory]
[InlineData("0,0,0", 0)]
[InlineData("0,1,2", 3)]
[InlineData("1,2,3", 6)]
public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add(input);

    Assert.Equal(expected, actual);
}

Używanie metod pomocnika zamiast instalacji i usuwania

Jeśli potrzebujesz podobnego obiektu lub stanu dla testów, użyj metody pomocniczej, a nie Setup i Teardown atrybutów, jeśli istnieją. Metody pomocnicze są preferowane nad tymi atrybutami z kilku powodów:

  • Mniej nieporozumień podczas odczytywania testów, ponieważ cały kod jest widoczny z poziomu każdego testu
  • Mniejsze prawdopodobieństwo skonfigurowania zbyt dużej lub zbyt małej ilości dla danego testu
  • Mniejsze prawdopodobieństwo udostępniania stanu między testami, co powoduje utworzenie niechcianych zależności między nimi

W ramach testów jednostkowych atrybut Setup jest wywoływany przed każdym testem jednostkowym w zestawie testów. Niektórzy programiści postrzegają to zachowanie jako przydatne, ale często prowadzi ono do rozrostu i utrudnia czytelność testów. Każdy test ma zazwyczaj inne wymagania dotyczące konfiguracji i wykonywania. Niestety atrybut Setup wymusza użycie dokładnie tych samych wymagań dla każdego testu.

Uwaga

Atrybuty SetUp i TearDown są usuwane w xUnit w wersji 2.x lub nowszej.

oryginalny kod

Stosowanie najlepszych rozwiązań

private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
    stringCalculator = new StringCalculator();
}
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var stringCalculator = CreateDefaultStringCalculator();

    var actual = stringCalculator.Add("0,1");

    Assert.Equal(1, actual);
}
// More tests...
// More tests...
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var result = stringCalculator.Add("0,1");

    Assert.Equal(1, result);
}
private StringCalculator CreateDefaultStringCalculator()
{
    return new StringCalculator();
}

Unikaj wielu zadań typu Act

Podczas pisania testów spróbuj uwzględnić tylko jedno zadanie Act na test. Niektóre typowe podejścia do implementowania pojedynczego zadania aktu obejmują utworzenie oddzielnego testu dla każdego aktu lub użycie testów sparametryzowanych. Istnieje kilka korzyści związanych z używaniem jednego zadania Act dla każdego testu:

  • Można łatwo rozpoznać, które zadanie Akt kończy się niepowodzeniem, jeśli test się nie powiedzie.
  • Możesz upewnić się, że test koncentruje się tylko na jednym przypadku.
  • Zyskasz jasny obraz, dlaczego testy kończą się niepowodzeniem.

Kilka zadań Akt musi być sprawdzanych indywidualnie, a nie można zagwarantować, że wszystkie zadania sprawdzania są wykonywane. W większości struktur testowania jednostkowego po niepowodaniu zadania Assert w teście jednostkowym wszystkie kolejne testy są automatycznie uznawane za zakończone niepowodzeniem. Proces może być mylący, ponieważ niektóre działające funkcje mogą być interpretowane jako zakończone niepowodzeniem.

oryginalny kod

[Fact]
public void Add_EmptyEntries_ShouldBeTreatedAsZero()
{
    // Act
    var actual1 = stringCalculator.Add("");
    var actual2 = stringCalculator.Add(",");

    // Assert
    Assert.Equal(0, actual1);
    Assert.Equal(0, actual2);
}

Stosowanie najlepszych rozwiązań

[Theory]
[InlineData("", 0)]
[InlineData(",", 0)]
public void Add_EmptyEntries_ShouldBeTreatedAsZero(string input, int expected)
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Act
    var actual = stringCalculator.Add(input);

    // Assert
    Assert.Equal(expected, actual);
}

Weryfikowanie metod prywatnych przy użyciu metod publicznych

W większości przypadków nie trzeba testować metody prywatnej w kodzie. Prywatne metody są elementem implementacji i nigdy nie mogą istnieć w izolacji. W pewnym momencie procesu programowania wprowadzasz publiczną metodę wywoływania metody prywatnej w ramach jej implementacji. Podczas pisania testów jednostkowych zależy ci na końcowym wyniku działania metody publicznej, która odwołuje się do metody prywatnej.

Rozważmy następujący scenariusz kodu:

public string ParseLogLine(string input)
{
    var sanitizedInput = TrimInput(input);
    return sanitizedInput;
}

private string TrimInput(string input)
{
    return input.Trim();
}

Jeśli chodzi o testowanie, pierwszą reakcją może być napisanie testu dla metody TrimInput, aby upewnić się, że działa zgodnie z oczekiwaniami. Istnieje jednak możliwość, że metoda ParseLogLine manipuluje obiektem sanitizedInput w sposób, którego nie oczekujesz. Nieznane zachowanie może uczynić test względem metody TrimInput bezużytecznym.

Lepszym testem w tym scenariuszu jest zweryfikowanie metody ParseLogLine dostępnej publicznie:

public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
{
    var parser = new Parser();

    var result = parser.ParseLogLine(" a ");

    Assert.Equals("a", result);
}

W przypadku napotkania metody prywatnej znajdź metodę publiczną, która wywołuje metodę prywatną, i napisz testy względem metody publicznej. Tylko dlatego, że metoda prywatna zwraca oczekiwany wynik, nie oznacza systemu, który ostatecznie wywołuje metodę prywatną, prawidłowo używa wyniku.

Obsługa statycznych atrap z wykorzystaniem szwów

Jedną z zasad testu jednostkowego jest to, że musi mieć pełną kontrolę nad testem systemu. Jednak ta zasada może być problematyczna, gdy kod produkcyjny zawiera wywołania do odwołań statycznych (na przykład DateTime.Now).

Zapoznaj się z następującym scenariuszem kodu:

public int GetDiscountedPrice(int price)
{
    if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}

Czy możesz napisać test jednostkowy dla tego kodu? Możesz spróbować uruchomić zadanie Assert w price:

public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(2, actual)
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(1, actual);
}

Niestety, szybko zdajesz sobie sprawę, że występują pewne problemy z testem:

  • Jeśli zestaw testów zostanie uruchomiony we wtorek, drugi test zakończy się pomyślnie, ale pierwszy test zakończy się niepowodzeniem.
  • Jeśli zestaw testów działa na inny dzień, pierwszy test zakończy się pomyślnie, ale drugi test zakończy się niepowodzeniem.

Aby rozwiązać te problemy, należy wprowadzić szew do kodu produkcyjnego. Jednym z podejść jest opakowanie kodu, który musisz kontrolować, w interfejsie i upewnienie się, że kod produkcyjny jest zależny od tego interfejsu.

public interface IDateTimeProvider
{
    DayOfWeek DayOfWeek();
}

public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
{
    if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}

Musisz również napisać nową wersję pakietu testowego:

public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Monday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(2, actual);
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(1, actual);
}

Teraz zestaw testów ma pełną kontrolę nad wartością DateTime.Now i może ustawić dowolną wartość podczas wywoływania metody.