Test Unitaire: DateTime.Maintenant
J'ai des tests unitaires qui s'attendent à ce que l'heure actuelle soit différente de DateTime.Maintenant et je ne veux pas changer l'Heure de l'ordinateur, évidemment.
Quelle est la meilleure stratégie pour y parvenir?
20 réponses
Le meilleurs stratégie est de envelopper l'heure actuelle dans une abstraction et injecter cette abstraction dans le consommateur.
alternativement , 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 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, lors de tests unitaires avec un état statique, rappelez-vous toujours de démolir votre appareil en appelant TimeProvider.ResetToDefault()
.
Ce sont toutes de bonnes réponses, c'est ce que j'ai fait sur un autre projet:
l'Utilisation de la:
Obtenez L'Heure de la date réelle d'Aujourd'hui
var today = SystemTime.Now().Date;
Au lieu D'utiliser DateTime.Maintenant, vous devez utiliser SystemTime.Now()
... Ce n'est pas un changement difficile, mais cette solution pourrait ne pas être idéale pour tous les projets.
Voyager dans le temps (laisse aller 5 ans dans le futur)
SystemTime.SetDateTime(today.AddYears(5));
Bénéficiez de Nos Faux "aujourd'hui" (de 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;
}
}
grains de beauté:
[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
}
avertissement - je travaille sur les taupes
Vous avez quelques options pour le faire:
Utilisez mocking framework et utilisez un DateTimeService (implémentez une petite classe wrapper et injectez-la dans le code de production). L'implémentation de wrapper accédera à DateTime et dans les tests, vous pourrez vous moquer de la classe wrapper.
Utiliser Typemock Isolateur, il peut faux DateTime.Maintenant et ne vous obligera pas à changer le code en cours de test.
Utiliser Taupes, il peut également faux DateTime.Maintenant et ne nécessitera pas de changement dans le code de production.
Quelques exemples:
classe Wrapper en 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();
}
Avertissement - je travaille à Typemock
Ajouter un faux assemblage pour le système (clic droit sur System reference = > 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();
}
Un thread-safe SystemClock
utiliser 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();
}
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 (système.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 à 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;
}
}
}
Pour tester un code qui dépend du System.DateTime
, le system.dll
doit être moqué.
Il y a deux cadres que je connais qui font cela. Microsoft faux et Blouse.
Microsoft fakes nécessite Visual studio 2012 ultimatum et fonctionne tout droit sorti du compton.
Blouse est une source ouverte et très facile à utiliser. Il peut être téléchargé en utilisant 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);
});
Objets Simulés.
Un DateTime simulé qui renvoie un Maintenant approprié pour votre test.
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 talons de test et les détours dans. NET basé sur les délégués. Les Moles peuvent être utilisées pour dévier 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 abstrait le DateTime dans un fournisseur. C'était juste mal et j'avais l'impression que c'était trop juste pour tester. Je vais mettre cela en œuvre dans mon projet personnel ce soir.
Je suis surpris que personne n'ait suggéré l'une des façons les plus évidentes d'aller:
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 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 réutiliser cela dans plusieurs classes.
EDIT: pour toute personne intéressée, cela s'appelle ajouter une "couture" à votre classe, un point où vous pouvez vous connecter à son comportement pour modifiez-le (à des fins de test ou autre) sans avoir à changer le code de la classe.
Une note spéciale sur la moquerie DateTime.Now
avec TypeMock ...
La valeur de DateTime.Now
doit être placée dans une variable pour que cela soit correctement moqué. 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))
La bonne pratique est, quand 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
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. parce que cela pourrait conduire à une mauvaise utilisation un jour. j'ai donc 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, at test project a fait un assistant qui traitera 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 le code
var dateTimeNow = DateTimeProvider.DateTime.Now //not DateTime.Now
var dateTimeUtcNow = DateTimeProvider.DateTime.UtcNow //not DateTime.UtcNow
Et sur les essais
[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 souvenir d'une chose, parfois le DateTime réel 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 déférence serait durée maximale .FromMilliseconds (0.00002) . Mais la plupart du temps, c'est encore moins
Trouvez l'échantillon à MockSamples
Une autre option qui n'est pas mentionnée est d'injecter l'heure actuelle à 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 variation interne est ouverte aux tests unitaires, parallèles ou non.
Ceci est basé sur le principe que ImplicitTimeDependencyMethod
est " trop simple à casser "(voir: http://junit.sourceforge.net/doc/faq/faq.htm#best_3 ) n'a donc pas besoin d'être inclus dans la couverture du test unitaire. Bien qu'il devrait être touché dans les tests d'intégration de toute façon.
Selon le but de la classe, il peut être souhaitable d'avoir ces deux méthodes publiques de toute façon.
Voici mon anwer de cette question. Je combine le modèle 'contexte ambiant' avec IDisposable. Vous pouvez donc utiliser DateTimeProvider.Courant 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;
}
}
}
En utilisant ITimeProvider
nous avons été obligés de le prendre dans 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 cherché le ITimeProvider
dans le framework. net. Nous avons cherché le paquet NuGet, et trouvé Un {[11] } qui ne peut pas fonctionner avec DateTimeOffset
.
Nous avons donc trouvé notre propre solution, qui ne dépend que des types de la 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 tester l'Unité
public void MakeIllustratingTuple_WhenCalled_FillsCreatedAt()
{
DateTimeOffset expected = CreateRandomDateTimeOffset();
DateTimeOffset StubNow() => expected;
var thing = new ThingThatNeedsTimeProvider(StubNow);
var (_, actual) = thing.MakeIllustratingTuple();
Assert.AreEqual(expected, actual);
}
Peut-être une solution moins professionnelle mais plus simple pourrait être de faire un paramètre DateTime à la méthode consumer.Par exemple , au lieu de make méthode comme SampleMethod, make SampleMethod1 avec paramètre.Le test de SampleMethod1 est plus facile
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)
{
}
}
Une façon propre de le faire est d'injecter VirtualTime. Il vous permet de contrôler le temps. Installez D'abord VirtualTime
Install-Package VirtualTime
Cela permet, par exemple, de faire du temps qui se déplace 5 fois plus vite sur tous les appels à DateTime.Maintenant ou UtcNow
var DateTime = DateTime.Now.ToVirtualTime(5);
Pour rendre le temps plus lent, par exemple 5 fois plus lent faire
var DateTime = DateTime.Now.ToVirtualTime(0.5);
Pour que le temps reste immobile, faites
var DateTime = DateTime.Now.ToVirtualTime(0);
Reculer dans le temps n'est pas encore testé
Voici un échantillon 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 vérifier plus de tests ici:
Quelle Date Heure.Maintenant.Tovirtualtime extension vous donne est une instance de ITime que vous passez à une méthode / classe qui dépend de ITime. certains DateTime.Maintenant.ToVirtualTime est configuré dans le conteneur DI de votre choix
Voici un autre exemple d'injection dans une classe contrustor
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);
}
Nous utilisions un objet SystemTime statique, mais nous avons rencontré des problèmes pour exécuter des tests unitaires parallèles. J'ai essayé d'utiliser la solution de Henk van Boeijen mais j'ai eu des problèmes à travers les threads asynchrones générés, j'ai fini par utiliser using 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;
}
}
Proviennent de https://gist.github.com/CraftyFella/42f459f7687b0b8b268fc311e6b4af08