Comment mocker DateTime dans les tests unitaires
Aujourd’hui, un développeur est venu me demander comment tester son code qui contient une référence à DateTime.Now
.
Effectivement, il arrive que votre application traite ses données différemment, en fonction de la date du jour.
Par exemple, comment vérifier le code suivant, qui dépend du trimestre courant ?
int trimester = (DateTime.Today.Month - 1) / 3 + 1;
if (trimester <= 2)
...
else
...
Injection de dépendance
Une façon propre de procéder, si vous utilisez l’injection de dépendance dans votre projet, est de créer une interface à injecter partout où vous souhaitez obtenir la date actuelle du système. Et de la définir, selon vos besoins, dans des tests unitaires.
public interface IDateTimeHelper
{
DateTime GetDateTimeNow();
}
public class DateTimeHelper : IDateTimeHelper
{
public DateTime GetDateTimeNow()
{
return DateTime.Now;
}
}
Cela fonctionne très bien, tant que vous utilisez l’injection de dépendance. Mais certaines personnes n’aiment pas injecter une classe aussi simple. De plus, que se passe-t-il si vous avez un code existant et que vous souhaitez simplement le remanier pour remplacer la date et l’heure ?
Modèle de contexte ambiant
Afin d’éviter l’injection d’une classe aussi simple que de donner la date et l’heure; Et pour simplifier la mise à jour de code existant, je propose une solution qui utilise le Modèle de contexte ambiant.
Pour cela, nous devons simplement utiliser une classe DateTimeProvider qui détermine
le contexte courant d’utilisation : DateTime.Now
est remplacé par DateTimeProvider.Now
.
int trimester = (DateTimeProvider.Today.Month - 1) / 3 + 1;
Ce provider retourne la date courante du système. Toutefois, en l’utilisant dans un test unitaire, nous pouvons adapter le contexte pour y préciser une date prédéfinie.
var result = DateTimeProvider.Now; // Returns DateTime.Now
var fakeDate = new DateTime(2018, 5, 26);
using (var context = new DateTimeProviderContext(fakeDate))
{
var result = DateTimeProvider.Now; // Returns 2018-05-26
}
Comme vous le voyez dans le code ci-dessus, la seule chose que nous devons faire pour simuler
la date actuelle du système, est d’envelopper notre appel de méthode dans un bloc using
.
Ce qui crée une nouvelle instance de DateTimeProviderContext
et de préciser la date souhaitée
comme argument du constructeur. C’est tout !
Code source
La classe DateTimeProvider
est :
public class DateTimeProvider
{
public static DateTime Now
=> DateTimeProviderContext.Current == null
? DateTime.Now
: DateTimeProviderContext.Current.ContextDateTimeNow;
public static DateTime UtcNow => Now.ToUniversalTime();
public static DateTime Today => Now.Date;
}
Et la classe DateTimeProviderContext
est :
public class DateTimeProviderContext : IDisposable
{
internal DateTime ContextDateTimeNow;
private static ThreadLocal<Stack> ThreadScopeStack = new ThreadLocal<Stack>(() => new Stack());
private Stack _contextStack = new Stack();
public DateTimeProviderContext(DateTime contextDateTimeNow)
{
ContextDateTimeNow = contextDateTimeNow;
ThreadScopeStack.Value.Push(this);
}
public static DateTimeProviderContext Current
{
get
{
if (ThreadScopeStack.Value.Count == 0)
return null;
else
return ThreadScopeStack.Value.Peek() as DateTimeProviderContext;
}
}
public void Dispose()
{
ThreadScopeStack.Value.Pop();
}
}
Pourquoi avons-nous utilisé ThreadLocal<Stack>
pour conserver notre pile ?
Parce que si nous effectuons nos tests en parallèle, ils vont s’exécuter dans des threads séparés
et comme nous utilisons un champ statique pour maintenir la pile, ils interféreraient les uns avec
les autres et provoqueraient des erreurs.
Inspiration de akazemis.