web-dev-qa-db-fra.com

Test unitaire: DateTime.Now

Certains tests unitaires prévoient que l'heure actuelle sera différente de DateTime. Maintenant, je ne veux évidemment pas modifier l'heure de l'ordinateur. 

Quelle est la meilleure stratégie pour y parvenir?

124
Pedro

La stratégie best consiste à envelopper l'heure actuelle dans une abstraction et en injecter cette abstraction au consommateur .


Alternativement, vous pouvez également définir une abstraction de temps comme un contexte Ambient:

public abstract class TimeProvider
{
    private static TimeProvider current =
        DefaultTimeProvider.Instance;

    public static TimeProvider Current
    {
       get { return TimeProvider.current; }
       set 
       {
           if (value == null)
           {
               throw new ArgumentNullException("value");
           }
           TimeProvider.current = value; 
       }
   }

   public abstract DateTime UtcNow { get; }

   public static void ResetToDefault()
   {    
       TimeProvider.current = DefaultTimeProvider.Instance;
   }            
}

Cela vous permettra de le consommer comme ceci:

var now = TimeProvider.Current.UtcNow;

Dans un test unitaire, vous pouvez remplacer TimeProvider.Current par un objet Test Double/Mock. Exemple utilisant Moq:

var timeMock = new Mock<TimeProvider>();
timeMock.SetupGet(tp => tp.UtcNow).Returns(new DateTime(2010, 3, 11));
TimeProvider.Current = timeMock.Object;

Cependant, lorsque vous testez une unité avec un état statique, n'oubliez pas de démonter votre appareil en appelant TimeProvider.ResetToDefault().

181
Mark Seemann

Ce sont toutes de bonnes réponses, voici ce que j'ai fait sur un projet différent:

Utilisation:

Obtenir la date réelle du jour

var today = SystemTime.Now().Date;

Au lieu d'utiliser DateTime.Now, vous devez utiliser SystemTime.Now()... Ce n'est pas un changement difficile, mais cette solution peut ne pas être idéale pour tous les projets.

Voyage dans le temps (permet d'aller dans 5 ans)

SystemTime.SetDateTime(today.AddYears(5));

Obtenez notre Fake "aujourd'hui" (dans 5 ans à partir d'aujourd'hui)

var fakeToday = SystemTime.Now().Date;

Réinitialiser la date

SystemTime.ResetDateTime();

/// <summary>
/// Used for getting DateTime.Now(), time is changeable for unit testing
/// </summary>
public static class SystemTime
{
    /// <summary> Normally this is a pass-through to DateTime.Now, but it can be overridden with SetDateTime( .. ) for testing or debugging.
    /// </summary>
    public static Func<DateTime> Now = () => DateTime.Now;

    /// <summary> Set time to return when SystemTime.Now() is called.
    /// </summary>
    public static void SetDateTime(DateTime dateTimeNow)
    {
        Now = () =>  dateTimeNow;
    }

    /// <summary> Resets SystemTime.Now() to return DateTime.Now.
    /// </summary>
    public static void ResetDateTime()
    {
        Now = () => DateTime.Now;
    }
}
47

Moles:

[Test]  
public void TestOfDateTime()  
{  
      var firstValue = DateTime.Now;
      MDateTime.NowGet = () => new DateTime(2000,1,1);
      var secondValue = DateTime.Now;
      Assert(firstValue > secondValue); // would be false if 'moleing' failed
}

Disclaimer - Je travaille sur les taupes

23
Peli

Vous avez quelques options pour le faire:

  1. Utilisez la structure mocking et utilisez un DateTimeService (implémentez une petite classe wrapper et injectez-la dans le code de production). L'implémentation du wrapper va accéder à DateTime et dans les tests, vous pourrez vous moquer de la classe du wrapper.

  2. Utilisez Typemock Isolator , il peut simuler DateTime.Now et ne vous demandera pas de changer le code testé.

  3. Utilisez Moles , il peut également simuler DateTime.Now et ne nécessitera pas de modification du code de production.

Quelques exemples:

Classe Wrapper utilisant Moq:

[Test]
public void TestOfDateTime()
{
     var mock = new Mock<IDateTime>();
     mock.Setup(fake => fake.Now)
         .Returns(new DateTime(2000, 1, 1));

     var result = new UnderTest(mock.Object).CalculateSomethingBasedOnDate();
}

public class DateTimeWrapper : IDateTime
{
      public DateTime Now { get { return DateTime.Now; } }
}

Faking DateTime directement en utilisant Isolator:

[Test]
public void TestOfDateTime()
{
     Isolate.WhenCalled(() => DateTime.Now).WillReturn(new DateTime(2000, 1, 1));

     var result = new UnderTest().CalculateSomethingBasedOnDate();
}

Disclaimer - Je travaille chez Typemock

16
Elisha

Ajoutez un faux assemblage pour système (cliquez avec le bouton droit de la souris sur Référence système => Ajouter un faux assemblage).

Et écrivez dans votre méthode de test:

using (ShimsContext.Create())
{
   System.Fakes.ShimDateTime.NowGet = () => new DateTime(2014, 3, 10);
   MethodThatUsesDateTimeNow();
}
9
dave

Un thread sûr SystemClock utilisant ThreadLocal<T> me convient parfaitement.

ThreadLocal<T> est disponible dans .Net Framework v4.0 et supérieur.

/// <summary>
/// Provides access to system time while allowing it to be set to a fixed <see cref="DateTime"/> value.
/// </summary>
/// <remarks>
/// This class is thread safe.
/// </remarks>
public static class SystemClock
{
    private static readonly ThreadLocal<Func<DateTime>> _getTime =
        new ThreadLocal<Func<DateTime>>(() => () => DateTime.Now);

    /// <inheritdoc cref="DateTime.Today"/>
    public static DateTime Today
    {
        get { return _getTime.Value().Date; }
    }

    /// <inheritdoc cref="DateTime.Now"/>
    public static DateTime Now
    {
        get { return _getTime.Value(); }
    }

    /// <inheritdoc cref="DateTime.UtcNow"/>
    public static DateTime UtcNow
    {
        get { return _getTime.Value().ToUniversalTime(); }
    }

    /// <summary>
    /// Sets a fixed (deterministic) time for the current thread to return by <see cref="SystemClock"/>.
    /// </summary>
    public static void Set(DateTime time)
    {
        if (time.Kind != DateTimeKind.Local)
            time = time.ToLocalTime();

        _getTime.Value = () => time;
    }

    /// <summary>
    /// Resets <see cref="SystemClock"/> to return the current <see cref="DateTime.Now"/>.
    /// </summary>
    public static void Reset()
    {
        _getTime.Value = () => DateTime.Now;
    }
}

Exemple d'utilisation:

[TestMethod]
public void Today()
{
    SystemClock.Set(new DateTime(2015, 4, 3));

    DateTime expectedDay = new DateTime(2015, 4, 2);
    DateTime yesterday = SystemClock.Today.AddDays(-1D);
    Assert.AreEqual(expectedDay, yesterday);

    SystemClock.Reset();
}
5
Henk van Boeijen

En ce qui concerne la réponse @crabcrusherclamcollector, l'utilisation de cette approche dans les requêtes EF pose un problème (System.NotSupportedException: le type de nœud d'expression LINQ 'Invoke' n'est pas pris en charge dans LINQ to Entities). J'ai modifié l'implémentation pour que:

public static class SystemTime
    {
        private static Func<DateTime> UtcNowFunc = () => DateTime.UtcNow;

        public static void SetDateTime(DateTime dateTimeNow)
        {
            UtcNowFunc = () => dateTimeNow;
        }

        public static void ResetDateTime()
        {
            UtcNowFunc = () => DateTime.UtcNow;
        }

        public static DateTime UtcNow
        {
            get
            {
                DateTime now = UtcNowFunc.Invoke();
                return now;
            }
        }
    }
5
marcinn

Pour tester un code qui dépend du System.DateTime, vous devez simuler le system.dll.

À ma connaissance, il existe deux cadres. Microsoft imite et Blouses .

Les contrefaçons Microsoft exigent l’ultimatum de visual studio 2012 et fonctionnent directement depuis le compton.

Smocks est une source ouverte et très facile à utiliser. Il peut être téléchargé en utilisant NuGet.

Ce qui suit montre une maquette de System.DateTime:

Smock.Run(context =>
{
  context.Setup(() => DateTime.Now).Returns(new DateTime(2000, 1, 1));

   // Outputs "2000"
   Console.WriteLine(DateTime.Now.Year);
});
3
persianLife

Une note spéciale sur le moquage DateTime.Now avec TypeMock ...

La valeur de DateTime.Now doit être placée dans une variable pour que celle-ci soit simulée correctement. Par exemple:

Cela ne fonctionne pas:

if ((DateTime.Now - message.TimeOpened.Value) > new TimeSpan(1, 0, 0))

Cependant, cela:

var currentDateTime = DateTime.Now;
if ((currentDateTime - message.TimeOpened.Value) > new TimeSpan(1, 0, 0))
2
Dave Black

J'ai rencontré le même problème, mais j'ai trouvé un projet de recherche de Microsoft qui résout ce problème.

http://research.Microsoft.com/en-us/projects/moles/

Moles est un framework léger pour les talons de test et les détours dans .NET basé sur des délégués. Les taupes peuvent être utilisées pour détourner n'importe quelle méthode .NET, y compris les méthodes non virtuelles/statiques dans des types scellés

// Let's detour DateTime.Now
MDateTime.NowGet = () => new DateTime(2000,1, 1);

if (DateTime.Now == new DateTime(2000, 1, 1);
{
    throw new Exception("Wahoo we did it!");
}

L'exemple de code a été modifié à partir de l'original.

J'avais fait ce que d'autres ont suggéré et résumé le DateTime dans un fournisseur. Je me sentais mal et j'avais l'impression que c'était trop pour les tests. Je vais mettre cela en pratique dans mon projet personnel ce soir.

2
Bobby Cannon

Faux Objets.

Un DateTime factice qui renvoie un message Now approprié à votre test.

2
S.Lott

Je suis surpris que personne n'ait suggéré l'un des moyens les plus évidents:

public class TimeDependentClass
{
    public void TimeDependentMethod(DateTime someTime)
    {
        if (GetCurrentTime() > someTime) DoSomething();
    }

    protected virtual DateTime GetCurrentTime()
    {
        return DateTime.Now; // or UtcNow
    }
}

Ensuite, vous pouvez simplement remplacer cette méthode dans votre test double.

J'aime aussi un peu injecter une classe TimeProvider dans certains cas, mais pour d'autres, c'est plus que suffisant. Je préférerais probablement la version TimeProvider si vous avez besoin de la réutiliser dans plusieurs classes.

EDIT: Pour ceux qui sont intéressés, il s’agit d’ajouter une "couture" à votre classe, un point où vous pouvez vous connecter à son comportement pour le modifier (à des fins de test ou autre) sans avoir à changer le code de la classe.

2
sara

La bonne pratique est, lorsque DateTimeProvider implémente IDisposable.

public class DateTimeProvider : IDisposable 
{ 
    [ThreadStatic] 
    private static DateTime? _injectedDateTime; 

    private DateTimeProvider() 
    { 
    } 

    /// <summary> 
    /// Gets DateTime now. 
    /// </summary> 
    /// <value> 
    /// The DateTime now. 
    /// </value> 
    public static DateTime Now 
    { 
        get 
        { 
            return _injectedDateTime ?? DateTime.Now; 
        } 
    } 

    /// <summary> 
    /// Injects the actual date time. 
    /// </summary> 
    /// <param name="actualDateTime">The actual date time.</param> 
    public static IDisposable InjectActualDateTime(DateTime actualDateTime) 
    { 
        _injectedDateTime = actualDateTime; 

        return new DateTimeProvider(); 
    } 

    public void Dispose() 
    { 
        _injectedDateTime = null; 
    } 
} 

Ensuite, vous pouvez injecter votre faux DateTime pour les tests unitaires

    using (var date = DateTimeProvider.InjectActualDateTime(expectedDateTime)) 
    { 
        var bankAccount = new BankAccount(); 

        bankAccount.DepositMoney(600); 

        var lastTransaction = bankAccount.Transactions.Last(); 

        Assert.IsTrue(expectedDateTime.Equals(bankAccount.Transactions[0].TransactionDate)); 
    } 

Voir exemple Exemple de DateTimeProvider

1
Mino

Une autre option non mentionnée consiste à injecter l'heure actuelle dans la méthode dépendante:

public class DateTimeNowDependencyClass
{
    ...

    public void ImplicitTimeDependencyMethod(Obj arg0)
    {
        this.TimeDependencyMethod(DateTime.Now, arg0);
    }

    internal void TimeDependencyMethod(DateTime now, Obj arg1)
    {
        ...
    }

    ...
}

La variante interne est ouverte aux tests unitaires, parallèles ou non.

Ceci est basé sur le principe que ImplicitTimeDependencyMethod est "trop ​​simple à décomposer" (voir: http://junit.sourceforge.net/doc/faq/faq.htm#best_3 ) donc n’a pas besoin d’être inclus dans couverture des tests unitaires. Quoiqu'il en soit, il devrait être abordé dans les tests d'intégration.

En fonction de l'objectif de la classe, il peut être souhaitable que ces deux méthodes soient publiques de toute façon.

0
amcc

J'ai eu le même problème, mais je pensais que nous ne devrions pas utiliser les choses set datetime sur la même classe. car cela pourrait conduire à une mauvaise utilisation un jour. donc j'ai utilisé le fournisseur comme

public class DateTimeProvider
{
    protected static DateTime? DateTimeNow;
    protected static DateTime? DateTimeUtcNow;

    public DateTime Now
    {
        get
        {
            return DateTimeNow ?? System.DateTime.Now;
        }
    }

    public DateTime UtcNow
    {
        get
        {
            return DateTimeUtcNow ?? System.DateTime.UtcNow;
        }
    }

    public static DateTimeProvider DateTime
    {
        get
        {
            return new DateTimeProvider();
        }
    }

    protected DateTimeProvider()
    {       
    }
}

Pour les tests, au projet de test fait un assistant qui traitera les choses définies,

public class MockDateTimeProvider : DateTimeProvider
{
    public static void SetNow(DateTime now)
    {
        DateTimeNow = now;
    }

    public static void SetUtcNow(DateTime utc)
    {
        DateTimeUtcNow = utc;
    }

    public static void RestoreAsDefault()
    {
        DateTimeNow = null;
        DateTimeUtcNow = null;
    }
}

sur le code

var dateTimeNow = DateTimeProvider.DateTime.Now         //not DateTime.Now
var dateTimeUtcNow = DateTimeProvider.DateTime.UtcNow   //not DateTime.UtcNow

et sur des tests

[Test]
public void Mocked_Now()
{
    DateTime now = DateTime.Now;
    MockDateTimeProvider.SetNow(now);    //set to mock
    Assert.AreEqual(now, DateTimeProvider.DateTime.Now);
    Assert.AreNotEqual(now, DateTimeProvider.DateTime.UtcNow);
}

[Test]
public void Mocked_UtcNow()
{
    DateTime utcNow = DateTime.UtcNow;
    MockDateTimeProvider.SetUtcNow(utcNow);   //set to mock
    Assert.AreEqual(utcNow, DateTimeProvider.DateTime.UtcNow);
    Assert.AreNotEqual(utcNow, DateTimeProvider.DateTime.Now);
}

Mais vous devez vous rappeler une chose, parfois le vrai DateTime et le DateTime du fournisseur n'agissent pas de la même manière

[Test]
public void Now()
{
    Assert.AreEqual(DateTime.Now.Kind, DateTimeProvider.DateTime.Now.Kind);
    Assert.LessOrEqual(DateTime.Now, DateTimeProvider.DateTime.Now);
    Assert.LessOrEqual(DateTimeProvider.DateTime.Now - DateTime.Now, TimeSpan.FromMilliseconds(1));
}

J'ai supposé que la retenue serait maximale TimeSpan.FromMilliseconds (0.00002). Mais la plupart du temps c'est encore moins

Trouvez l'échantillon sur MockSamples

0
Dipon Roy

En utilisant ITimeProvider, nous avons été obligés de l'intégrer au projet special partagé commun qui doit être référencé à partir du reste des projets. Mais cela compliquait le contrôle des dépendances .

Nous avons recherché la ITimeProvider dans le framework .NET. Nous avons cherché le paquet NuGet et trouvé un qui ne peut pas fonctionner avec DateTimeOffset.

Nous avons donc créé notre propre solution, qui ne dépend que des types de bibliothèque standard. Nous utilisons une instance de Func<DateTimeOffset>.

Comment utiliser

public class ThingThatNeedsTimeProvider
{
    private readonly Func<DateTimeOffset> now;
    private int nextId;

    public ThingThatNeedsTimeProvider(Func<DateTimeOffset> now)
    {
        this.now = now;
        this.nextId = 1;
    }

    public (int Id, DateTimeOffset CreatedAt) MakeIllustratingTuple()
    {
        return (nextId++, now());
    }
}

Comment s'inscrire

Autofac

builder.RegisterInstance<Func<DateTimeOffset>>(() => DateTimeOffset.Now);

(Pour les futurs éditeurs: ajoutez vos cas ici).

Comment faire des tests unitaires

public void MakeIllustratingTuple_WhenCalled_FillsCreatedAt()
{
    DateTimeOffset expected = CreateRandomDateTimeOffset();
    DateTimeOffset StubNow() => expected;
    var thing = new ThingThatNeedsTimeProvider(StubNow);

    var (_, actual) = thing.MakeIllustratingTuple();

    Assert.AreEqual(expected, actual);
}
0
Mark Shevchenko

Voici ma réponse à cette question. Je combine le motif 'Ambient Context' avec IDisposable. Vous pouvez donc utiliser DateTimeProvider.Current dans votre code de programme normal et dans le test, vous remplacez la portée par une instruction using. 

using System;
using System.Collections.Immutable;


namespace ambientcontext {

public abstract class DateTimeProvider : IDisposable
{
    private static ImmutableStack<DateTimeProvider> stack = ImmutableStack<DateTimeProvider>.Empty.Push(new DefaultDateTimeProvider());

    protected DateTimeProvider()
    {
        if (this.GetType() != typeof(DefaultDateTimeProvider))
            stack = stack.Push(this);
    }

    public static DateTimeProvider Current => stack.Peek();
    public abstract DateTime Today { get; }
    public abstract DateTime Now {get; }

    public void Dispose()
    {
        if (this.GetType() != typeof(DefaultDateTimeProvider))
            stack = stack.Pop();
    }

    // Not visible Default Implementation 
    private class DefaultDateTimeProvider : DateTimeProvider {
        public override DateTime Today => DateTime.Today; 
        public override DateTime Now => DateTime.Now; 
    }
}
}

Voici comment utiliser le DateTimeProvider ci-dessus dans un test unitaire 

using System;
using Xunit;

namespace ambientcontext
{
    public class TestDateTimeProvider
    {
        [Fact]
        public void TestDateTime()
        {
            var actual = DateTimeProvider.Current.Today;
            var expected = DateTime.Today;

            Assert.Equal<DateTime>(expected, actual);

            using (new MyDateTimeProvider(new DateTime(2012,12,21)))
            {
                Assert.Equal(2012, DateTimeProvider.Current.Today.Year);

                using (new MyDateTimeProvider(new DateTime(1984,4,4)))
                {
                    Assert.Equal(1984, DateTimeProvider.Current.Today.Year);    
                }

                Assert.Equal(2012, DateTimeProvider.Current.Today.Year);
            }

            // Fall-Back to Default DateTimeProvider 
            Assert.Equal<int>(expected.Year,  DateTimeProvider.Current.Today.Year);
        }

        private class MyDateTimeProvider : DateTimeProvider 
        {
            private readonly DateTime dateTime; 

            public MyDateTimeProvider(DateTime dateTime):base()
            {
                this.dateTime = dateTime; 
            }

            public override DateTime Today => this.dateTime.Date;

            public override DateTime Now => this.dateTime;
        }
    }
}
0
Stefc

Une méthode simple consiste à injecter VirtualTime. Cela vous permet de contrôler le temps. Première installation de VirtualTime

Install-Package VirtualTime

Cela permet, par exemple, de gagner un temps cinq fois plus rapide pour tous les appels vers DateTime.Now ou UtcNow

var DateTime = DateTime.Now.ToVirtualTime(5);

Pour que le temps avance plus lentement, par exemple 5 fois plus lentement, faites

var DateTime = DateTime.Now.ToVirtualTime(0.5);

Pour faire taire le temps 

var DateTime = DateTime.Now.ToVirtualTime(0);

Reculer dans le temps n'est pas encore testé

Voici un exemple de test:

[TestMethod]
public void it_should_make_time_move_faster()
{
    int speedOfTimePerMs = 1000;
    int timeToPassMs = 3000;
    int expectedElapsedVirtualTime = speedOfTimePerMs * timeToPassMs;
    DateTime whenTimeStarts = DateTime.Now;
    ITime time = whenTimeStarts.ToVirtualTime(speedOfTimePerMs);
    Thread.Sleep(timeToPassMs);
    DateTime expectedTime = DateTime.Now.AddMilliseconds(expectedElapsedVirtualTime - timeToPassMs);
    DateTime virtualTime = time.Now;

    Assert.IsTrue(TestHelper.AreEqualWithinMarginOfError(expectedTime, virtualTime, MarginOfErrorMs));
}

Vous pouvez consulter plus de tests ici:

https://github.com/VirtualTime/VirtualTime/blob/master/VirtualTimeLib.Tests/when_virtual_time_is_used.cs

Ce que l'extension DateTime.Now.ToVirtualTime vous donne est une instance d'ITime que vous transmettez à une méthode/classe qui dépend d'ITime. une dateTime.Now.ToVirtualTime est configurée dans le conteneur DI de votre choix

Voici un autre exemple d'injection dans un contrôleur de classe

public class AlarmClock
{
    private ITime DateTime;
    public AlarmClock(ITime dateTime, int numberOfHours)
    {
        DateTime = dateTime;
        SetTime = DateTime.UtcNow.AddHours(numberOfHours);
        Task.Run(() =>
        {
            while (!IsAlarmOn)
            {
                IsAlarmOn = (SetTime - DateTime.UtcNow).TotalMilliseconds < 0;
            }
        });
    }
    public DateTime SetTime { get; set; }
    public bool IsAlarmOn { get; set; }
}

[TestMethod]
public void it_can_be_injected_as_a_dependency()
{
    //virtual time has to be 1000*3.75 faster to get to an hour 
    //in 1000 ms real time
    var dateTime = DateTime.Now.ToVirtualTime(1000 * 3.75);
    var numberOfHoursBeforeAlarmSounds = 1;
    var alarmClock = new AlarmClock(dateTime, numberOfHoursBeforeAlarmSounds);
    Assert.IsFalse(alarmClock.IsAlarmOn);
    System.Threading.Thread.Sleep(1000);
    Assert.IsTrue(alarmClock.IsAlarmOn);
}
0
Samuel

Nous utilisions un objet SystemTime statique, mais nous avons rencontré des problèmes lors de l'exécution de tests unitaires parallèles. J'ai essayé d'utiliser la solution de Henk van Boeijen, mais des problèmes se sont produits au niveau des threads asynchrones. Nous avons fini par utiliser AsyncLocal de la manière suivante:

public static class Clock
{
    private static Func<DateTime> _utcNow = () => DateTime.UtcNow;

    static AsyncLocal<Func<DateTime>> _override = new AsyncLocal<Func<DateTime>>();

    public static DateTime UtcNow => (_override.Value ?? _utcNow)();

    public static void Set(Func<DateTime> func)
    {
        _override.Value = func;
    }

    public static void Reset()
    {
        _override.Value = null;
    }
}

Source: https://Gist.github.com/CraftyFella/42f459f7687b0b8b268fc311e6b4af08

0
rrrr

Peut-être une solution moins professionnelle mais plus simple pourrait être de créer un paramètre DateTime selon la méthode consommateur.

public void SampleMethod()
    {
        DateTime anotherDateTime = DateTime.Today.AddDays(-10);
        if ((DateTime.Now-anotherDateTime).TotalDays>10)
        {

        }
    }
    public void SampleMethod1(DateTime dateTimeNow)
    {
        DateTime anotherDateTime = DateTime.Today.AddDays(-10);
        if ((dateTimeNow - anotherDateTime).TotalDays > 10)
        {

        }

    }
0
Mehmet