Test unitaire: DateTime.Now

164

J'ai quelques tests unitaires qui s'attendent à ce que «l'heure actuelle» soit différente de DateTime.Now et je ne veux évidemment pas changer l'heure de l'ordinateur.

Quelle est la meilleure stratégie pour y parvenir?

Pedro
la source
stocker une ancienne valeur dans l'heure actuelle et la comparer avec datetime.now, car les tests unitaires utilisent de fausses données, vous pouvez facilement le faire, n'est-ce pas?
Prashant Lakhlani
3
Fournir une abstraction de DateTime actuelle est utile non seulement pour les tests et le débogage, mais également dans le code de production - cela dépend des besoins de l'application.
Pavel Hodek
1
N'utilisez pas de classe statique pour obtenir le DateTime
Daniel Little
et une autre approche simple utilisant VirtualTime ci
Samuel
Une option qui pourrait fonctionner et vous pouvez ajouter une surcharge est de créer une surcharge de la méthode qui accepte le DateTime. Ce serait celui que vous testez, la méthode existante appellerait simplement la surcharge avec now.
Phil Cooper

Réponses:

219

La meilleure stratégie consiste à envelopper l'heure actuelle dans une abstraction et à injecter cette abstraction dans le consommateur .


Vous pouvez également définir une abstraction temporelle en tant que contexte ambiant :

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 le remplacer TimeProvider.Currentpar 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, lors de tests unitaires avec un état statique, n'oubliez pas de démonter votre appareil en appelant TimeProvider.ResetToDefault().

Mark Seemann
la source
7
Dans le cas d'un contexte ambiant, même si vous démolissez, comment faites-vous vos tests en parallèle?
Ilya Chernomordik
2
@IlyaChernomordik Vous ne devriez pas avoir à injecter ILogger ou d'autres problèmes transversaux , donc injecter un fournisseur de temps reste la meilleure option.
Mark Seemann
5
@MikeK Comme le dit la première phrase de ma réponse: la meilleure stratégie est d'envelopper l'heure actuelle dans une abstraction et d'injecter cette abstraction dans le consommateur. Tout le reste dans cette réponse est une deuxième meilleure alternative.
Mark Seemann
3
Salut. Moi Noob. Comment empêcher les gens d'accéder à DateTime.UtcNow avec cette solution? Il serait facile pour quelqu'un de manquer l'utilisation du TimeProvider. Menant ainsi à des bogues dans les tests?
Obbles
1
Revues de code @Obbles (ou programmation par paires, qui est une forme de révision de code en direct). Je suppose que l'on pourrait écrire un outil qui analyse la base de code DateTime.UtcNowet similaire, mais les révisions de code sont quand même une bonne idée.
Mark Seemann
58

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

Usage:

Obtenir l'heure réelle de la date d'aujourd'hui

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 n'est peut-être pas idéale pour tous les projets.

Voyage dans le temps (partons 5 ans dans le futur)

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

Get Our Fake "aujourd'hui" (dans 5 ans à compter 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;
    }
}
crabeCRAUSHERclamCOLLECTOR
la source
1
Merci, j'aime cette approche fonctionnelle. Aujourd'hui (deux ans plus tard) j'utilise toujours une version modifiée de la classe TimeProvider (vérifiez la réponse acceptée), cela fonctionne vraiment bien.
Pedro
7
@crabCRUSHERclamCOLI: Si vous avez l'idée de ayende.com/blog/3408/dealing-with-time-in-tests , alors c'est une bonne chose de faire un lien vers elle.
Johann Gerell
3
Ce concept fonctionne également très bien pour se moquer de la génération de Guid (public static Func <Guid> NewGuid = () => Guid.NewGuid ();
mdwhatcott
2
Vous pouvez également ajouter cette méthode utile: public static void ResetDateTime (DateTime dateTimeNow) {var timespanDiff = TimeSpan.FromTicks (DateTime.Now.Ticks - dateTimeNow.Ticks); Now = () => DateTime.Now - timespanDiff; }
Pavel Hodek
17
très dangereux Cela peut affecter d'autres tests unitaires exécutés en parallèle.
Amete Blessed
24

Taupes:

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

Clause de non-responsabilité - Je travaille sur les taupes

Peli
la source
1
malheureusement ne fonctionne pas avec nunit et resharper.
odyth
On dirait que Moles n'est plus pris en charge, remplacé par Fakes msdn.microsoft.com/en-us/library/ ... , sans aucun doute MS arrêtera cela trop tôt
danio
17

Vous avez quelques options pour le faire:

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

  2. Utilisez Typemock Isolator , il peut simuler DateTime.Now et ne vous obligera pas à modifier le code testé.

  3. Utilisez Moles , il peut également simuler DateTime.Now et ne nécessitera pas de changement de 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; } }
}

Fake DateTime directement en utilisant Isolator:

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

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

Clause de non-responsabilité - Je travaille chez Typemock

Élisée
la source
12

Ajouter un faux assemblage pour le système (clic droit 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();
}
Dave
la source
si vous avez accès à Vs Premium ou Ultimate C'est de loin le moyen le plus simple / le plus rapide de le faire.
Rugdr
7

En ce qui concerne la réponse @crabcrusherclamcollector, il y a un problème lors de l'utilisation de cette approche dans les requêtes EF (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 cela:

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;
            }
        }
    }
Marcinn
la source
Je suppose que par EF, vous voulez dire Entity Framework? À quoi ressemble l'objet sur lequel vous utilisez SystemTime? Je suppose que vous avez une référence à SystemTime sur l'entité réelle, à la place, vous devriez utiliser un objet DateTime régulier sur votre entité et le définir à l'aide de SystemTime partout où cela a du sens dans votre application
crabCRUSHERclamCOLLECTOR
1
Oui, je l'ai testé lors de la création de la requête linq et de EntityFramework6 et du référencement dans cette requête UtcNow de SystemTime. Il y avait une exception comme décrit. Après avoir changé la mise en œuvre, cela fonctionne bien.
marcinn
J'adore cette solution! Génial!
mirind4
7

Pour tester un code qui dépend de System.DateTime, le system.dlldoit être simulé.

Il y a deux cadres que je connais qui font cela. Microsoft faux et smocks .

Les faux Microsoft nécessitent un ultimatum de Visual Studio 2012 et fonctionnent directement à partir du compton.

Smocks est un open source et très facile à utiliser. Il peut être téléchargé à l'aide de NuGet.

Ce qui suit montre une simulation de System.DateTime:

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

   // Outputs "2000"
   Console.WriteLine(DateTime.Now.Year);
});
persianLife
la source
5

Une SystemClockutilisation sans fil ThreadLocal<T>fonctionne très bien pour moi.

ThreadLocal<T> est disponible dans le .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();
}
Henk van Boeijen
la source
1
+! Thread local doit éviter les problèmes avec l'exécution de tests parallèles et plusieurs tests susceptibles de remplacer l' Nowinstance actuelle .
Hux
1
J'ai essayé de l'utiliser mais j'ai eu un problème entre les threads, donc j'ai opté pour un principe similaire mais en utilisant AsyncLocalplutôt queThreadLocal
rrrr
@rrrr: pouvez-vous expliquer le problème que vous avez rencontré?
Henk van Boeijen
@HenkvanBoeijen Bien sûr, nous avons rencontré des problèmes lorsque l'heure a été définie, puis nous attendions une méthode asynchrone. Console.WriteLine(SystemClock.Now); SystemClock.Set(new DateTime(2017, 01, 01)); Console.WriteLine(SystemClock.Now); await Task.Delay(1000); Console.WriteLine(SystemClock.Now);
rrrr
@HenkvanBoeijen J'ai ajouté une réponse avec ce que j'ai fini par utiliser lien
rrrr
3

J'ai rencontré ce 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 stubs de test et les détours dans .NET basé sur des délégués. Les taupes peuvent être utilisées pour détourner toute méthode .NET, y compris les méthodes non virtuelles / statiques dans les 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é par rapport à l'original.

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

Bobby Cannon
la source
2
BTW, cela ne fonctionne qu'avec VS2010. J'étais bouleversé quand j'ai découvert que Moles était désormais Fakes pour VS2012 et qu'il n'était disponible que pour Premium et Ultimate Visual Studios. Pas de faux pour VS 2012 Professional. Donc je n'ai jamais eu à utiliser ça. :(
Bobby Cannon
3

Une note spéciale sur les moqueries DateTime.Nowavec TypeMock ...

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

Cela ne fonctionne pas:

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

Cependant, cela fait:

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

Objets simulés.

Un DateTime simulé qui renvoie un Now qui convient à votre test.

S.Lott
la source
2

Je suis surpris que personne n'ait suggéré l'une des façons les plus évidentes de procéder:

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 TimeProviderclasse dans certains cas, mais pour d'autres, c'est plus que suffisant. Je préférerais probablement la TimeProviderversion si vous devez la réutiliser dans plusieurs classes.

EDIT: Pour toute personne intéressée, cela s'appelle l'ajout d'une "couture" à votre classe, un point où vous pouvez vous connecter à son comportement pour le modifier (à des fins de test ou autrement) sans avoir à changer le code de la classe.

Sara
la source
1

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 l'exemple Exemple de DateTimeProvider

Mino
la source
1

Une manière propre de faire ceci est d'injecter VirtualTime. Cela vous permet de contrôler le temps. Installez d'abord VirtualTime

Install-Package VirtualTime

Cela permet, par exemple, de rendre le temps qui passe 5 fois plus vite sur tous les appels à DateTime.Now ou UtcNow

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

Pour ralentir le temps, par exemple 5 fois plus lentement, faites

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

Pour que le temps s'arrête, faites

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

Le recul 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

L'extension DateTime.Now.ToVirtualTime vous donne une instance d'ITime que vous passez à une méthode / classe qui dépend d'ITime. certains DateTime.Now.ToVirtualTime sont configurés 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);
}
Samuel
la source
Il y a aussi une bonne lecture sur le code de test qui dépend de DateTime par Ayende ici ayende.com/blog/3408/dealing-with-time-in-tests
Samuel
1

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 j'ai eu des problèmes sur les threads asynchrones générés, j'ai fini par utiliser AsyncLocal d'une manière similaire à celle ci-dessous:

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

Tiré de https://gist.github.com/CraftyFella/42f459f7687b0b8b268fc311e6b4af08

rrrr
la source
1

Une vieille question, mais toujours d'actualité.

Mon approche consiste à créer une nouvelle interface et une nouvelle classe pour encapsuler l' System.DateTime.Nowappel

public interface INow
{
    DateTime Execute();
}

public sealed class Now : INow
{
    public DateTime Execute()
    {
        return DateTime.Now
    }
}

Cette interface peut être injectée dans n'importe quelle classe qui a besoin d'obtenir la date et l'heure actuelles. Dans cet exemple, j'ai une classe qui ajoute une période à la date et à l'heure actuelles (une unité testable System.DateTime.Now.Add (TimeSpan))

public interface IAddTimeSpanToCurrentDateAndTime
{
    DateTime Execute(TimeSpan input);
}

public class AddTimeSpanToCurrentDateAndTime : IAddTimeSpanToCurrentDateAndTime
{
    private readonly INow _now;

    public AddTimeSpanToCurrentDateAndTime(INow now)
    {
        this._now = now;
    }

    public DateTime Execute(TimeSpan input)
    {
        var currentDateAndTime = this._now.Execute();

        return currentDateAndTime.Add(input);
    }
}

Et des tests peuvent être écrits pour s'assurer qu'il fonctionne correctement. J'utilise NUnit et Moq mais n'importe quel framework de test fera l'affaire

public class Execute
{
    private Moq.Mock<INow> _nowMock;

    private AddTimeSpanToCurrentDateAndTime _systemUnderTest;

    [SetUp]
    public void Initialize()
    {
        this._nowMock = new Moq.Mock<INow>(Moq.MockBehavior.Strict);

        this._systemUnderTest = AddTimeSpanToCurrentDateAndTime(
            this._nowMock.Object);
    }

    [Test]
    public void AddTimeSpanToCurrentDateAndTimeExecute0001()
    {
        // arrange

        var input = new TimeSpan(911252);

        // arrange : mocks

        this._nowMock
            .Setup(a => a.Execute())
            .Returns(new DateTime(348756););

        // arrange : expected

        var expected = new DateTime(911252 + 348756);

        // act

        var actual = this._systemUnderTest.Execute(input).Result;

        // assert

        Assert.Equals(actual, expected);
    }
}

Ce modèle fonctionne pour toutes les fonctions qui dépendent de facteurs externes, comme System.Random.Next(), System.DateTime.Now.UtcNow, System.Guid.NewGuid()etc.

Voir https://loadlimited.visualstudio.com/Stamina/_git/Stamina.Core pour plus d'exemples ou obtenir le package https://www.nuget.org/packages/Stamina.Core nuget.

Kevin Brydon
la source
1

Vous pouvez changer la classe que vous testez pour utiliser une Func<DateTime>qui sera passée à travers ses paramètres de constructeur, donc lorsque vous créez une instance de la classe dans du code réel, vous pouvez passer () => DateTime.UtcNowauFunc<DateTime> paramètre, et sur le test, vous pouvez passer le temps que vous souhaite tester.

Par exemple:

    [TestMethod]
    public void MyTestMethod()
    {
        var instance = new MyClass(() => DateTime.MinValue);
        Assert.AreEqual(instance.MyMethod(), DateTime.MinValue);
    } 

    public void RealWorldInitialization()
    {
        new MyClass(() => DateTime.UtcNow);
    }

    class MyClass
    {
        private readonly Func<DateTime> _utcTimeNow;

        public MyClass(Func<DateTime> UtcTimeNow)
        {
            _utcTimeNow = UtcTimeNow;
        }

        public DateTime MyMethod()
        {
            return _utcTimeNow();
        }
    }
Nir
la source
1
J'aime cette réponse. D'après ce que j'ai compris, cela pourrait être surprenant dans le monde C # où l'accent est davantage mis sur les classes / objets. Quoi qu'il en soit, je préfère cela parce que c'est vraiment simple et clair pour moi ce qui se passe.
pseudoramble
0

J'ai eu le même problème, mais je pensais que nous ne devrions pas utiliser les éléments datetime définis sur la même classe. car cela pourrait conduire un jour à une mauvaise utilisation. 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, lors du projet de test, vous avez créé un assistant qui s'occupera des 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 code

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

et sur les 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 il faut se rappeler une chose, parfois le DateTime réel et le DateTime du fournisseur ne fonctionnent 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 déférence serait maximale TimeSpan.FromMilliseconds (0.00002) . Mais la plupart du temps c'est encore moins

Trouvez l'échantillon sur MockSamples

Dipon Roy
la source
0

Voici ma réponse à cette question. Je combine le modèle 'Ambient Context' avec IDisposable. Ainsi, vous pouvez utiliser DateTimeProvider.Current dans votre code de programme normal et dans le test, vous remplacez la portée avec 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;
        }
    }
}
Stefc
la source
0

En utilisant, ITimeProvidernous avons été obligés de le prendre dans un projet commun partagé spécial qui doit être référencé à partir du reste des autres projets. Mais cela a compliqué le contrôle des dépendances .

Nous avons recherché le ITimeProviderdans le framework .NET. Nous avons recherché le package NuGet et en avons trouvé un qui ne fonctionne pas avec DateTimeOffset.

Nous avons donc proposé notre propre solution, qui ne dépend que des types de bibliothèque standard. Nous utilisons une instance deFunc<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 effectuer un test unitaire

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

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

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

Peut-être qu'une solution moins professionnelle mais plus simple pourrait être de créer un paramètre DateTime à la méthode du consommateur.Par exemple, au lieu de faire une méthode comme SampleMethod, créez SampleMethod1 avec le paramètre.

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)
        {

        }

    }
Mehmet
la source