Was ist ein guter Weg, um DateTime.Now während des Tests zu überschreiben?

116

Ich habe einen (C #) Code, der sich auf das heutige Datum stützt, um die zukünftigen Dinge richtig zu berechnen. Wenn ich das heutige Datum für den Test verwende, muss ich die Berechnung im Test wiederholen, was sich nicht richtig anfühlt. Wie kann das Datum im Test am besten auf einen bekannten Wert gesetzt werden, damit ich testen kann, ob das Ergebnis ein bekannter Wert ist?

Craig.Nicol
quelle

Antworten:

157

Ich bevorzuge Klassen, die Zeit verwenden und tatsächlich auf einer Schnittstelle basieren, wie z

interface IClock
{
    DateTime Now { get; } 
}

Mit einer konkreten Umsetzung

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

Wenn Sie möchten, können Sie jede andere Art von Uhr zum Testen bereitstellen, z

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

Die Bereitstellung der Uhr für die darauf basierende Klasse kann mit einem gewissen Aufwand verbunden sein, der jedoch von einer beliebigen Anzahl von Abhängigkeitsinjektionslösungen (unter Verwendung eines Inversion of Control-Containers, einer einfachen alten Konstruktor- / Setter-Injektion oder sogar eines statischen Gateway-Musters) verarbeitet werden kann ).

Andere Mechanismen zur Bereitstellung eines Objekts oder einer Methode, die die gewünschten Zeiten liefern, funktionieren ebenfalls, aber ich denke, das Wichtigste ist, ein Zurücksetzen der Systemuhr zu vermeiden, da dies nur auf anderen Ebenen zu Schmerzen führen wird.

Die Verwendung DateTime.Nowund Einbeziehung in Ihre Berechnungen fühlt sich nicht nur nicht richtig an, sondern raubt Ihnen auch die Möglichkeit, bestimmte Zeiten zu testen, z. B. wenn Sie einen Fehler entdecken, der nur in der Nähe einer Mitternachtsgrenze oder dienstags auftritt. Wenn Sie die aktuelle Zeit verwenden, können Sie diese Szenarien nicht testen. Oder zumindest nicht wann immer Sie wollen.

Blair Conrad
quelle
2
Wir haben dies tatsächlich in einer der xUnit.net-Erweiterungen formalisiert. Wir haben eine Clock-Klasse, die Sie als statische und nicht als DateTime verwenden, und Sie können die Uhr "einfrieren" und "auftauen", auch für bestimmte Daten. Siehe is.gd/3xds und is.gd/3xdu
Brad Wilson
2
Es ist auch erwähnenswert, dass Sie, wenn Sie Ihre Systemuhrmethode ersetzen möchten - dies geschieht beispielsweise, wenn Sie eine globale Uhr in einem Unternehmen mit Niederlassungen in weit verstreuten Zeitzonen verwenden -, diese Methode wertvolle Freiheit auf Unternehmensebene bietet, die zu ändern Bedeutung von "Jetzt".
Mike Burton
1
Dieser Weg funktioniert für mich sehr gut, zusammen mit der Verwendung eines Dependency Injection Framework, um zur IClock-Instanz zu gelangen.
Wilka
8
Gute Antwort. Ich wollte nur hinzufügen, dass in fast allen Fällen UtcNowverwendet und dann entsprechend den Anforderungen des Codes angepasst werden sollte, z. B. Geschäftslogik, Benutzeroberfläche usw. Die Manipulation von DateTime über Zeitzonen hinweg ist ein Minenfeld, aber der beste erste Schritt ist, immer damit zu beginnen UTC-Zeit.
Adam Ralph
@BradWilson Diese Links sind jetzt defekt. Konnte sie auch nicht auf WayBack hochziehen.
55

Ayende Rahien verwendet eine statische Methode, die ziemlich einfach ist ...

public static class SystemTime
{
    public static Func<DateTime> Now = () => DateTime.Now;
}
Anthony Mastrean
quelle
1
Es scheint gefährlich, den Stub / Mock-Punkt zu einer öffentlichen globalen Variablen (statische Klassenvariable) zu machen. Wäre es nicht besser, es nur auf das zu testende System zu beschränken - z. B. es zu einem privaten statischen Mitglied der zu testenden Klasse zu machen?
Aaron
1
Es ist eine Frage des Stils. Dies ist das Mindeste, was Sie tun können, um eine durch Unit-Test änderbare Systemzeit zu erhalten.
Anthony Mastrean
3
IMHO wird bevorzugt die Verwendung einer Schnittstelle anstelle globaler statischer Singletons. Stellen Sie sich das folgende Szenario vor: Der Testläufer ist effizient und führt so viele Tests wie möglich parallel aus. Ein Test ändert sich auf die angegebene Zeit auf X, der andere auf Y. Wir haben jetzt einen Konflikt und diese beiden Tests schalten Fehler um. Wenn wir Schnittstellen verwenden, verspottet jeder Test die Schnittstelle gemäß seinen Anforderungen. Jeder Test ist jetzt von anderen Tests isoliert. HTH.
ShloEmi
17

Ich denke, eine separate Uhrenklasse für etwas Einfaches wie das Abrufen des aktuellen Datums zu erstellen, ist etwas übertrieben.

Sie können das heutige Datum als Parameter übergeben, um ein anderes Datum in den Test einzugeben. Dies hat den zusätzlichen Vorteil, dass Ihr Code flexibler wird.

Mendelt
quelle
Ich habe sowohl auf deine als auch auf Blairs Antworten geantwortet, obwohl beide gegensätzlich sind. Ich denke, beide Ansätze sind gültig. Ihr Ansatz Ich würde wahrscheinlich ein Projekt verwenden, das so etwas wie Unity nicht verwendet.
RichardOD
1
Ja, es ist möglich, einen Parameter für "jetzt" hinzuzufügen. In einigen Fällen müssen Sie jedoch einen Parameter verfügbar machen, den Sie normalerweise nicht verfügbar machen möchten. Nehmen wir zum Beispiel an, Sie haben eine Methode, mit der die Tage zwischen einem Datum und jetzt berechnet werden. Dann möchten Sie "jetzt" nicht als Parameter verfügbar machen, da dies die Manipulation Ihres Ergebnisses ermöglicht. Wenn es sich um Ihren eigenen Code handelt, fügen Sie "jetzt" als Parameter hinzu. Wenn Sie jedoch in einem Team arbeiten, wissen Sie nie, wofür andere Entwickler Ihren Code verwenden. Wenn also "jetzt" ein kritischer Teil Ihres Codes ist, müssen Sie dies tun Schützen Sie es vor Manipulation oder Missbrauch.
Stitch10925
17

Die Verwendung von Microsoft Fakes zum Erstellen eines Shims ist eine sehr einfache Möglichkeit, dies zu tun. Angenommen, ich hätte die folgende Klasse:

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

}

In Visual Studio 2012 können Sie Ihrem Testprojekt eine Fakes-Assembly hinzufügen, indem Sie mit der rechten Maustaste auf die Assembly klicken, für die Sie Fakes / Shims erstellen möchten, und "Fakes-Assembly hinzufügen" auswählen.

Fakes Assembly hinzufügen

Zum Schluss würde die Testklasse folgendermaßen aussehen:

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
quelle
1
Genau das habe ich gesucht. Vielen Dank! Übrigens, funktioniert genauso in VS 2013.
Douglas Ludlow
Oder heutzutage VS 2015 Enterprise Edition. Was für eine so gute Praxis eine Schande ist.
RJB
12

Der Schlüssel zum erfolgreichen Unit-Test ist die Entkopplung . Sie müssen Ihren interessanten Code von seinen externen Abhängigkeiten trennen, damit er isoliert getestet werden kann. (Glücklicherweise erzeugt testgetriebene Entwicklung entkoppelten Code.)

In diesem Fall ist Ihr externes Datum die aktuelle DateTime.

Mein Rat hier ist, die Logik, die sich mit der DateTime befasst, auf eine neue Methode oder Klasse oder was auch immer in Ihrem Fall sinnvoll ist, zu extrahieren und die DateTime zu übergeben. Jetzt kann Ihr Komponententest eine beliebige DateTime übergeben, um vorhersagbare Ergebnisse zu erzielen.

Jay Bazuzi
quelle
10

Eine andere mit Microsoft Moles ( Isolation Framework für .NET ).

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

Moles ermöglicht das Ersetzen einer beliebigen .NET-Methode durch einen Delegaten. Moles unterstützt statische oder nicht virtuelle Methoden. Moles verlässt sich auf den Profiler von Pex.

João Angelo
quelle
Das ist wunderschön, erfordert aber Visual Studio 2010! :-(
Pandincus
Es funktioniert auch in VS 2008 einwandfrei. Es funktioniert jedoch am besten mit MSTest. Sie können NUnit verwenden, aber dann müssen Sie Ihre Tests mit einem speziellen Testläufer ausführen.
Torbjørn
Ich würde Moles (auch bekannt als Microsoft Fakes) nach Möglichkeit vermeiden. Im Idealfall sollte es nur für Legacy-Code verwendet werden, der noch nicht über die Abhängigkeitsinjektion getestet werden kann.
Brianpeiris
1
@brianpeiris, was sind die Nachteile bei der Verwendung von Microsoft Fakes?
Ray Cheng
Sie können nicht immer Inhalte von Drittanbietern DI verwenden, daher ist Fakes für Unit-Tests durchaus akzeptabel, wenn Ihr Code eine API von Drittanbietern aufruft, die Sie für einen Unit-Test nicht instanziieren möchten. Ich bin damit einverstanden, Fakes / Moles nicht für Ihren eigenen Code zu verwenden, aber es ist für andere Zwecke durchaus akzeptabel.
ChrisCW
5

Ich würde vorschlagen, ein IDisposable-Muster zu verwenden:

[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);
    } 
}

Im Detail hier beschrieben: http://www.lesnikowski.com/blog/index.php/testing-datetime-now/

Pawel Lesnikowski
quelle
2

Sie können die Klasse (besser: Methode / Delegat ), für die Sie DateTime.Nowin der getesteten Klasse verwenden, einfügen. HabenDateTime.Now ein Standardwert und setzen Sie ihn beim Testen nur auf eine Dummy-Methode, die einen konstanten Wert zurückgibt.

EDIT: Was Blair Conrad gesagt hat (er hat einen Code zum Anschauen). Außer, ich bevorzuge Delegierte dafür, da sie Ihre Klassenhierarchie nicht mit Dingen wie IClock...

Daren Thomas
quelle
1

Ich war so oft mit dieser Situation konfrontiert, dass ich ein einfaches Nuget erstellt habe, das die Now- Eigenschaft über die Schnittstelle verfügbar macht .

public interface IDateTimeTools
{
    DateTime Now { get; }
}

Die Implementierung ist natürlich sehr einfach

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

Nachdem ich meinem Projekt Nuget hinzugefügt habe, kann ich es in den Unit-Tests verwenden

Geben Sie hier die Bildbeschreibung ein

Sie können das Modul direkt über den GUI Nuget Package Manager oder mit dem folgenden Befehl installieren:

Install-Package -Id DateTimePT -ProjectName Project

Und der Code für das Nuget ist hier .

Das Beispiel für die Verwendung mit dem Autofac finden Sie hier .

Pawel Wujczyk
quelle
-11

Haben Sie darüber nachgedacht, die bedingte Kompilierung zu verwenden, um zu steuern, was während des Debuggens / Bereitstellens passiert?

z.B

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

Wenn Sie dies nicht tun, möchten Sie die Eigenschaft verfügbar machen, damit Sie sie manipulieren können. Dies ist alles Teil der Herausforderung, testbaren Code zu schreiben , was ich derzeit selbst ringe: D.

Bearbeiten

Ein großer Teil von mir würde Blairs Ansatz bevorzugen . Auf diese Weise können Sie Teile des Codes "Hot Plug", um das Testen zu erleichtern. Es folgt alles dem Entwurfsprinzip , das die Unterschiede zusammenfasst unterscheidet. Es unterscheidet sich nicht vom Produktionscode. Es wird nur niemand jemals extern sehen.

Das Erstellen und die Benutzeroberfläche scheinen für dieses Beispiel jedoch eine Menge Arbeit zu sein (weshalb ich mich für die bedingte Kompilierung entschieden habe).

Rob Cooper
quelle
Wow, du wurdest von dieser Antwort hart getroffen. das ist , was ich getan habe, obwohl an einem einzigen Ort, wo ich Methoden aufrufen , die zu ersetzen DateTimeist Nowund Todayusw.
Dave Cousineau