Qual è un buon modo per sovrascrivere DateTime.Now durante il test?


116

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

Risposte:


157

La mia preferenza è che le classi che utilizzano il tempo si basino effettivamente su un'interfaccia, come

interface IClock
{
    DateTime Now { get; } 
}

Con un'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

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

Potrebbe esserci un sovraccarico nel fornire l'orologio alla classe che si basa su di esso, ma ciò potrebbe essere gestito da un numero qualsiasi di soluzioni di inserimento delle dipendenze (utilizzando un contenitore di inversione del controllo, una semplice vecchia iniezione di costruttore / setter o persino un modello di gateway statico ).

Funzionano anche altri meccanismi per fornire un oggetto o un metodo che fornisce i tempi desiderati, ma penso che la cosa fondamentale sia evitare di ripristinare l'orologio di sistema, poiché questo introdurrà dolore su altri livelli.

Inoltre, utilizzarlo DateTime.Nowe includerlo nei tuoi calcoli non solo non ti sembra giusto, ma ti priva della capacità di testare orari particolari, ad esempio se scopri un bug che si verifica solo vicino a un confine di mezzanotte o il martedì. L'utilizzo dell'ora corrente non ti consentirà di testare questi scenari. O almeno non quando vuoi.


2
Lo abbiamo effettivamente formalizzato in una delle estensioni di xUnit.net. Abbiamo una classe Clock che usi come statica piuttosto che DateTime e puoi "congelare" e "scongelare" l'orologio, anche per date specifiche. Vedi is.gd/3xds e is.gd/3xdu
Brad Wilson,

2
Vale anche la pena notare che quando si desidera sostituire il metodo dell'orologio di sistema, ad esempio quando si utilizza un orologio globale in un'azienda con filiali in fusi orari molto dispersi, questo metodo offre una preziosa libertà a livello aziendale di modificare il significato di "Now".
Mike Burton

1
In questo modo funziona molto bene per me, insieme all'utilizzo di un framework di iniezione delle dipendenze per arrivare all'istanza di IClock.
Wilka

8
Bella risposta. Volevo solo aggiungere che in quasi tutti i casi UtcNowdovrebbe essere utilizzato e quindi regolato in modo appropriato in base alle preoccupazioni del codice, ad es. Logica aziendale, interfaccia utente, ecc. Ora UTC.
Adam Ralph

@BradWilson Quei collegamenti ora sono interrotti. Non sono riuscito nemmeno a tirarli su su WayBack.

55

Ayende Rahien utilizza un metodo statico piuttosto semplice ...

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

1
Sembra pericoloso rendere il punto stub / mock una variabile globale pubblica (variabile statica di classe). Non sarebbe meglio definirlo solo per il sistema sotto test, ad esempio rendendolo un membro statico privato della classe sotto test?
Aaron

1
È una questione di stile. Questa è l' ultima cosa che puoi fare per ottenere un tempo di sistema modificabile da test unitario.
Anthony Mastrean

3
IMHO, è preferibile utilizzare l'interfaccia invece dei singleton statici globali. Considera il seguente scenario: il test runner è efficiente ed esegue il maggior numero di test possibile in parallelo, un test cambia a tempo dato su X, l'altro su Y. Ora abbiamo un conflitto e questi due test alterneranno gli errori. Se usiamo interfacce, ogni test prende in giro l'interfaccia in base alle sue esigenze, ogni test è ora isolato dagli altri test. HTH.
ShloEmi

17

Penso che creare una classe 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 l'ulteriore vantaggio di rendere il tuo codice più flessibile.


Ho fatto +1 su entrambe le risposte tua e di Blair, anche se entrambe si oppongono. Penso che entrambi gli approcci siano validi. Il tuo approccio probabilmente userei un progetto che non usa qualcosa come Unity.
RichardOD

1
Sì, è possibile aggiungere un parametro per "adesso". Tuttavia, in alcuni casi ciò richiede di esporre un parametro che normalmente non si desidera esporre. Supponiamo, ad esempio, che tu abbia un metodo che calcola i giorni tra una data e adesso, quindi non vuoi esporre "adesso" come parametro perché questo permette la manipolazione del tuo risultato. Se è il tuo codice, aggiungi "adesso" come parametro, ma se lavori in un team non sai mai per cosa gli altri sviluppatori useranno il tuo codice, quindi se "adesso" è una parte critica del tuo codice devi proteggerlo da manipolazione o uso improprio.
Stitch10925

17

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

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

}

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

Aggiunta di assemblaggi falsi

Infine, ecco come sarebbe la classe 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);

        }
    }
}
}

1
Questo era esattamente quello che stavo cercando. Grazie! A proposito, funziona allo stesso modo in VS 2013.
Douglas Ludlow

O in questi giorni, VS 2015 Enterprise edition. Il che è un peccato, per una tale migliore pratica.
RJB

12

La chiave per un test unitario di successo è il disaccoppiamento . Devi separare il tuo codice interessante dalle sue dipendenze esterne, in modo che possa essere testato isolatamente. (Fortunatamente, Test-Driven Development 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 classe o qualsiasi altra cosa abbia senso nel tuo caso e passare il DateTime. Ora, il tuo unit test può passare un DateTime arbitrario, per produrre risultati prevedibili.


10

Un altro che utilizza Microsoft Moles ( framework di isolamento per .NET ).

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

Moles consente di sostituire qualsiasi metodo .NET con un delegato. Le talpe supportano metodi statici o non virtuali. Moles si affida al profiler di Pex.


Questo è bellissimo, ma richiede Visual Studio 2010! :-(
Pandincus

Funziona bene anche in VS 2008. Tuttavia, funziona meglio con MSTest. Puoi usare NUnit, ma penso che tu debba eseguire i tuoi test con uno speciale test runner.
Torbjørn

Eviterei di usare Moles (aka Microsoft Fakes) quando possibile. Idealmente, dovrebbe essere utilizzato solo per codice legacy che non è già testabile tramite inserimento di dipendenze.
brianpeiris

1
@brianpeiris, quali sono gli svantaggi dell'utilizzo di Microsoft Fakes?
Ray Cheng

Non puoi sempre eseguire il DI di contenuti di terze parti, quindi Fakes è perfettamente accettabile per unit test se il tuo codice chiama un'API di terze parti che non desideri istanziare per uno unit test. Accetto di evitare di utilizzare Fakes / Moles per il tuo codice, ma è perfettamente accettabile per altri scopi.
ChrisCW



2

Potresti iniettare la classe (meglio: metodo / delegato ) che usi per DateTime.Nowla classe in esame. Devono DateTime.Nowessere un valore predefinito e impostarlo solo in testing su un metodo fittizio che restituisce un valore costante.

MODIFICARE: Quello che ha detto Blair Conrad (ha del codice da guardare). Tranne, tendo a preferire i delegati per questo, poiché non ingombrano la tua gerarchia di classe con cose come IClock...


1

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 negli unit test

inserisci qui la descrizione dell'immagine

È possibile installare il modulo direttamente dalla GUI Nuget Package Manager o utilizzando il comando:

Install-Package -Id DateTimePT -ProjectName Project

E il codice per il Nuget è qui .

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


-11

Hai considerato l'utilizzo della compilazione condizionale per controllare cosa accade durante il debug / la distribuzione?

per esempio

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

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

modificare

Gran parte di me preferirebbe l'approccio di Blair . Ciò consente di "collegare a caldo" parti del codice per facilitare i test. Tutto segue il principio di progettazione che racchiude ciò che varia codice di test non è diverso dal codice di produzione, è solo che nessuno lo vede mai esternamente.

La creazione e l'interfaccia possono sembrare un sacco di lavoro per questo esempio (motivo per cui ho optato per la compilazione condizionale).


wow sei stato colpito duramente da questa risposta. questo è quello che ho fatto, anche se in un unico luogo, dove io chiamo i metodi che sostituiscono DateTime's Nowed Todayecc
Dave Cousineau
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.