English
Français

Blog of Denis VOITURON

for a better .NET world

Comment mocker DateTime dans les tests unitaires

Posted on 2020-01-22

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.

Langues

EnglishEnglish
FrenchFrançais

Suivez-moi

Articles récents