Jaki jest dobry sposób na zastąpienie DateTime.Now podczas testowania?

116

Mam kod (C #), który opiera się na dzisiejszej dacie, aby poprawnie obliczyć rzeczy w przyszłości. Jeśli używam dzisiejszej daty w teście, muszę powtórzyć obliczenia w teście, co nie wydaje się właściwe. Jaki jest najlepszy sposób ustawienia daty na znaną wartość w teście, aby sprawdzić, czy wynik jest znaną wartością?

Craig.Nicol
źródło

Odpowiedzi:

157

Wolę mieć klasy, które wykorzystują czas, w rzeczywistości opierają się na interfejsie, takim jak

interface IClock
{
    DateTime Now { get; } 
}

Z konkretną realizacją

class SystemClock: IClock
{
     DateTime Now { get { return DateTime.Now; } }
}

Następnie, jeśli chcesz, możesz dostarczyć do testowania dowolny inny rodzaj zegara, taki jak

class StaticClock: IClock
{
     DateTime Now { get { return new DateTime(2008, 09, 3, 9, 6, 13); } }
}

Dostarczanie zegara do klasy, która na nim polega, może wiązać się z pewnym narzutem, ale może to być obsługiwane przez dowolną liczbę rozwiązań iniekcji zależności (przy użyciu kontenera Inversion of Control, zwykłego wstrzyknięcia starego konstruktora / ustawiacza lub nawet statycznego wzorca bramy ).

Inne mechanizmy dostarczania obiektu lub metody, które zapewniają pożądane czasy, również działają, ale myślę, że kluczową rzeczą jest unikanie resetowania zegara systemowego, ponieważ spowoduje to tylko ból na innych poziomach.

Ponadto używanie go DateTime.Nowi uwzględnianie w obliczeniach nie tylko nie wydaje się właściwe - pozbawia Cię możliwości testowania określonych godzin, na przykład jeśli odkryjesz błąd, który występuje tylko w pobliżu granicy północy lub we wtorki. Korzystanie z bieżącego czasu nie pozwoli Ci przetestować tych scenariuszy. A przynajmniej nie kiedy chcesz.

Blair Conrad
źródło
2
W rzeczywistości sformalizowaliśmy to w jednym z rozszerzeń xUnit.net. Mamy klasę Clock, której używasz jako statyczną, a nie DateTime, i możesz „zamrażać” i „rozmrażać” zegar, włączając w to określone daty. Zobacz is.gd/3xds i is.gd/3xdu
Brad Wilson,
2
Warto również zauważyć, że gdy zechcesz zastąpić metodę zegara systemowego - stanie się to na przykład przy korzystaniu z zegara globalnego w przedsiębiorstwie z oddziałami w bardzo rozproszonych strefach czasowych - ta metoda daje cenną biznesową swobodę zmiany znaczenie „teraz”.
Mike Burton
1
Ten sposób działa dla mnie naprawdę dobrze, wraz z wykorzystaniem Dependency Injection Framework, aby dostać się do instancji IClock.
Wilka
8
Świetna odpowiedź. Chciałem tylko dodać, że w prawie wszystkich przypadkach UtcNownależy go użyć, a następnie odpowiednio dostosować do obaw związanych z kodem, np. Logiki biznesowej, interfejsu użytkownika itp. Manipulowanie datą i czasem w różnych strefach czasowych jest polem minowym, ale najlepszym pierwszym krokiem do przodu jest zawsze zacząć od Czas UTC.
Adam Ralph
@BradWilson Te linki są teraz uszkodzone. Nie mogłem też ich wyciągnąć na WayBack.
55

Ayende Rahien stosuje metodę statyczną, która jest raczej prosta ...

public static class SystemTime
{
    public static Func<DateTime> Now = () => DateTime.Now;
}
Anthony Mastrean
źródło
1
Uczynienie punktu pośredniego / mock jako publicznej zmiennej globalnej (zmienna statyczna klasy) wydaje się niebezpieczne. Czy nie byłoby lepiej ograniczyć go tylko do testowanego systemu - np. Uczynić go prywatnym statycznym składnikiem testowanej klasy?
Aaron,
1
To kwestia stylu. Jest to najmniejsza rzecz, jaką możesz zrobić, aby uzyskać czas systemowy, który można zmienić za pomocą testów jednostkowych.
Anthony Mastrean
3
IMHO, lepiej jest używać interfejsu zamiast globalnych statycznych singletonów. Rozważ następujący scenariusz: Uruchamiający testy jest wydajny i przeprowadza równolegle tyle testów, ile może, jeden test zmienia się w zadanym czasie na X, a drugi na Y. Mamy teraz konflikt i te dwa testy będą przełączać niepowodzenia. Jeśli używamy interfejsów, każdy test szykuje interfejs zgodnie z jego potrzebami, każdy test jest teraz odizolowany od innego testu. HTH.
ShloEmi
17

Myślę, że utworzenie oddzielnej klasy zegara dla czegoś prostego, takiego jak uzyskanie aktualnej daty, jest trochę przesadą.

Możesz przekazać dzisiejszą datę jako parametr, aby móc wprowadzić inną datę w teście. Ma to dodatkową zaletę, że Twój kod jest bardziej elastyczny.

Mendelt
źródło
Dałem +1 dla odpowiedzi Twojej i Blair, mimo że obie są przeciwne. Myślę, że oba podejścia są prawidłowe. Twoje podejście Prawdopodobnie użyłbym projektu, który nie używa czegoś takiego jak Unity.
RichardOD
1
Tak, można dodać parametr „teraz”. Jednak w niektórych przypadkach wymaga to ujawnienia parametru, którego normalnie nie chcesz ujawniać. Załóżmy na przykład, że masz metodę, która oblicza dni między datą a teraz, więc nie chcesz pokazywać „teraz” jako parametru, ponieważ umożliwia to manipulowanie wynikiem. Jeśli to Twój własny kod, dodaj „teraz” jako parametr, ale jeśli pracujesz w zespole, nigdy nie wiesz, do czego inni programiści będą używać Twojego kodu, więc jeśli „teraz” jest krytyczną częścią kodu, musisz chronić go przed manipulacją lub niewłaściwym użyciem.
Stitch10925
17

Używanie Microsoft Fakes do tworzenia podkładek jest naprawdę łatwym sposobem na zrobienie tego. Załóżmy, że mam następującą klasę:

public class MyClass
{
    public string WhatsTheTime()
    {
        return DateTime.Now.ToString();
    }

}

W programie Visual Studio 2012 możesz dodać fałszywy zestaw do projektu testowego, klikając prawym przyciskiem myszy zestaw, dla którego chcesz utworzyć podróbki / podkładki, i wybierając opcję „Dodaj fałszywy zespół”

Dodawanie fałszywego montażu

Wreszcie, oto jak wyglądałaby klasa testowa:

using System;
using ConsoleApplication11;
using Microsoft.QualityTools.Testing.Fakes;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DateTimeTest
{
[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestWhatsTheTime()
    {

        using(ShimsContext.Create()){

            //Arrange
            System.Fakes.ShimDateTime.NowGet =
            () =>
            { return new DateTime(2010, 1, 1); };

            var myClass = new MyClass();

            //Act
            var timeString = myClass.WhatsTheTime();

            //Assert
            Assert.AreEqual("1/1/2010 12:00:00 AM",timeString);

        }
    }
}
}
mmilleruva
źródło
1
To było dokładnie to, czego szukałem. Dzięki! BTW, działa tak samo w VS 2013.
Douglas Ludlow
Lub obecnie VS 2015 Enterprise Edition. A szkoda, jak na taką najlepszą praktykę.
RJB
12

Kluczem do pomyślnego testowania jednostkowego jest oddzielenie . Musisz oddzielić interesujący kod od jego zewnętrznych zależności, aby można go było przetestować w izolacji. (Na szczęście programowanie sterowane testami tworzy oddzielony kod).

W tym przypadku Twoim zewnętrznym jest bieżący DateTime.

Moja rada jest taka, aby wyodrębnić logikę, która zajmuje się DateTime, do nowej metody lub klasy lub cokolwiek, co ma sens w twoim przypadku, i przekazać DateTime w. Teraz twój test jednostkowy może przekazać dowolny DateTime w celu uzyskania przewidywalnych wyników.

Jay Bazuzi
źródło
10

Kolejny wykorzystujący Microsoft Moles ( framework Isolation dla .NET ).

MDateTime.NowGet = () => new DateTime(2000, 1, 1);

Moles pozwala na zastąpienie dowolnej metody .NET delegatem. Mole obsługują metody statyczne lub niewirtualne. Moles polega na profilerze firmy Pex.

João Angelo
źródło
To piękne, ale wymaga Visual Studio 2010! :-(
Pandincus
Działa dobrze również w VS 2008. Jednak działa najlepiej z MSTest. Możesz użyć NUnit, ale myślę, że musisz przeprowadzić testy ze specjalnym testerem.
Torbjørn,
Unikałbym używania Moles (aka Microsoft Fakes), jeśli to możliwe. W idealnym przypadku należy go używać tylko w przypadku starszego kodu, którego nie można jeszcze przetestować za pomocą iniekcji zależności.
brianpeiris
1
@brianpeiris, jakie są wady korzystania z Microsoft Fakes?
Ray Cheng,
Nie możesz zawsze DI zawartości strony trzeciej, więc Fakes jest całkowicie akceptowalny do testów jednostkowych, jeśli kod wywołuje interfejs API innej firmy, którego nie chcesz tworzyć dla testu jednostkowego. Zgadzam się unikać używania Fakes / Moles dla własnego kodu, ale jest to całkowicie dopuszczalne do innych celów.
ChrisCW
5

Sugerowałbym użycie wzorca IDisposable:

[Test] 
public void CreateName_AddsCurrentTimeAtEnd() 
{
    using (Clock.NowIs(new DateTime(2010, 12, 31, 23, 59, 00)))
    {
        string name = new ReportNameService().CreateName(...);
        Assert.AreEqual("name 2010-12-31 23:59:00", name);
    } 
}

Szczegółowo opisane tutaj: http://www.lesnikowski.com/blog/index.php/testing-datetime-now/

Paweł Leśnikowski
źródło
2

Możesz wstrzyknąć klasę (lepiej: metodę / delegata ), której używasz DateTime.Noww testowanej klasie. Mają DateTime.Nowbyć wartością domyślną i ustawiać ją tylko podczas testowania na metodę fikcyjną, która zwraca wartość stałą.

EDYCJA: Co powiedział Blair Conrad (ma kod do obejrzenia). Z wyjątkiem tego, że wolę delegatów do tego, ponieważ nie zaśmiecają twojej hierarchii klas takimi rzeczami jak IClock...

Daren Thomas
źródło
1

Tak często spotykałem się z tą sytuacją, że stworzyłem prosty nuget, który ujawnia właściwość Now poprzez interfejs.

public interface IDateTimeTools
{
    DateTime Now { get; }
}

Wdrożenie jest oczywiście bardzo proste

public class DateTimeTools : IDateTimeTools
{
    public DateTime Now => DateTime.Now;
}

Więc po dodaniu nuget do mojego projektu mogę go używać w testach jednostkowych

wprowadź opis obrazu tutaj

Możesz zainstalować moduł bezpośrednio z GUI Nuget Package Manager lub używając polecenia:

Install-Package -Id DateTimePT -ProjectName Project

A kod do Nuget jest tutaj .

Przykład użycia z Autofac można znaleźć tutaj .

Paweł Wujczyk
źródło
-11

Czy rozważałeś użycie kompilacji warunkowej do kontrolowania tego, co dzieje się podczas debugowania / wdrażania?

na przykład

DateTime date;
#if DEBUG
  date = new DateTime(2008, 09, 04);
#else
  date = DateTime.Now;
#endif

Jeśli to się nie powiedzie, chcesz ujawnić właściwość, aby móc nią manipulować, to wszystko jest częścią wyzwania związanego z pisaniem testowalnego kodu, z czym obecnie się zmagam: D

Edytować

Duża część mnie wolałaby podejście Blaira . Pozwala to na „podłączanie na gorąco” części kodu, aby ułatwić testowanie. Wszystko to jest zgodne z zasadą projektową, hermetyzacja tego, co zmienia kod testowy, nie różni się od kodu produkcyjnego, po prostu nikt nigdy nie widzi go na zewnątrz.

Tworzenie i interfejs może wydawać się jednak pracochłonne w tym przykładzie (dlatego zdecydowałem się na kompilację warunkową).

Rob Cooper
źródło
wow, trafiłeś mocno w tę odpowiedź. to jest to, co robiłem, chociaż w jednym miejscu, gdzie ja nazywam metod, które zastępują DateTime„s Nowi Todayitp
Dave Cousineau