Qual è un buon modo per sovrascrivere DateTime.Ora durante i test?

Ho un codice (C #) che si basa sulla data di oggi per calcolare correttamente le cose in futuro. Se uso la data odierna nel test, devo ripetere il calcolo nel test, che non sembra corretto. Qual è il modo migliore per impostare la data su un valore noto all’interno del test in modo che possa verificare che il risultato sia un valore noto?

La mia preferenza è che le classi che utilizzano il tempo si basino effettivamente su un’interfaccia, ad esempio

interface IClock { DateTime Now { get; } } 

Con una implementazione concreta

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

Quindi, se lo desideri, puoi fornire qualsiasi altro tipo di orologio che desideri per il test, come ad esempio

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

Potrebbe esserci un sovraccarico nel fornire l’orologio alla class che fa affidamento su di esso, ma che potrebbe essere gestito da un numero qualsiasi di soluzioni di iniezione dipendenti (utilizzando un contenitore Inversion of Control, una semplice vecchia iniezione costruttore / setter o persino un modello di gateway statico ).

Altri meccanismi di consegna di un object o di un metodo che forniscono i tempi desiderati funzionano anche, ma penso che la cosa fondamentale sia evitare di reimpostare l’orologio di sistema, poiché questo introdurrà dolore ad altri livelli.

Inoltre, l’uso di DateTime.Now e di includerlo nei calcoli non solo non sembra corretto, ma ti priva della capacità di testare determinate ore, ad esempio se scopri un errore che si verifica solo vicino a un confine di mezzanotte o il martedì. L’utilizzo dell’ora corrente non ti consentirà di testare tali scenari. O almeno non quando vuoi.

Ayende Rahien usa un metodo statico piuttosto semplice …

 public static class SystemTime { public static Func Now = () => DateTime.Now; } 

Penso che creare una class di clock separata per qualcosa di semplice come ottenere la data corrente sia un po ‘eccessivo.

Puoi passare la data odierna come parametro in modo da poter inserire una data diversa nel test. Questo ha il vantaggio di rendere il tuo codice più flessibile.

Utilizzare Microsoft Fakes per creare uno shim è un modo davvero semplice per farlo. Supponiamo di avere la seguente class:

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

In Visual Studio 2012 è ansible aggiungere un assembly Fakes al progetto di test facendo clic con il pulsante destro del mouse sull’assieme per cui si desidera creare Fakes / Shim e selezionando “Add Fakes Assembly”

Aggiunta dell'assemblaggio di falsi

Infine, ecco come sarà la class di test:

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

La chiave per il successo dei test unitari è il disaccoppiamento . Devi separare il tuo codice interessante dalle sue dipendenze esterne, in modo che possa essere testato separatamente. (Fortunatamente, lo sviluppo basato su test produce codice disaccoppiato).

In questo caso, il tuo esterno è il DateTime corrente.

Il mio consiglio qui è di estrarre la logica che si occupa del DateTime in un nuovo metodo o class o qualsiasi cosa abbia senso nel tuo caso e passare il DateTime in. Ora, il tuo test unitario può passare un DateTime arbitrario in, per produrre risultati prevedibili.

Un altro utilizzando Microsoft Moles ( isolamento framework per. NET ).

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

Moles consente di sostituire qualsiasi metodo .NET con un delegato. Moles supporta metodi statici o non virtuali. Moles fa affidamento sul profiler di Pex.

Io suggerirei di utilizzare un modello 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); } } 

In dettaglio qui descritto: http://www.lesnikowski.com/blog/index.php/testing-datetime-now/

Risposta semplice: ditch System.DateTime 🙂 Invece, usa NodaTime e la sua libreria di test: NodaTime.Testing .

Ulteriori letture:

  • Test unitario con Noda Time
  • Iniezione di dipendenza: iniettare le dipendenze, periodo!

È ansible inserire la class (meglio: metodo / delegato ) che si utilizza per DateTime.Now nella class sottoposta a test. Avere DateTime.Now essere un valore predefinito e impostarlo solo in testing su un metodo dummy che restituisce un valore costante.

EDIT: Che cosa ha detto Blair Conrad (ha un codice da guardare). Tranne, io tendo a preferire i delegati per questo, poiché non ingombrano la gerarchia delle classi con cose come IClock

Ho affrontato questa situazione così spesso, che ho creato un semplice nuget che espone la proprietà Now tramite l’interfaccia.

 public interface IDateTimeTools { DateTime Now { get; } } 

L’implementazione è ovviamente molto semplice

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

Quindi, dopo aver aggiunto nuget al mio progetto, posso usarlo nei test unitari

inserisci la descrizione dell'immagine qui

È ansible installare il modulo direttamente dal Gestore pacchetti Nuget della GUI o utilizzando il comando:

 Install-Package -Id DateTimePT -ProjectName Project 

E il codice per il Nuget è qui .

L’esempio di utilizzo con Autofac può essere trovato qui .

Hai considerato di utilizzare la compilazione condizionale per controllare cosa succede durante il debug / deployment?

per esempio

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

In caso contrario, si desidera esporre la proprietà in modo da poterla manipolare, questo è tutto parte della sfida di scrivere codice verificabile , che è qualcosa che sto attualmente lottando: D

modificare

Una grande parte di me preferirebbe l’approccio di Blair . Ciò consente di “hot plug” parti del codice per facilitare il test. Tutto segue il principio di progettazione che incapsula ciò che varia il codice di test non è diverso dal codice di produzione, solo che nessuno lo vede mai esternamente.

La creazione e l’interfaccia potrebbero sembrare un sacco di lavoro per questo esempio (è per questo che ho optato per la compilazione condizionale).