Quel est un bon moyen de remplacer DateTime.Now pendant les tests?

116

J'ai du code (C #) qui s'appuie sur la date d'aujourd'hui pour calculer correctement les choses à l'avenir. Si j'utilise la date d'aujourd'hui dans le test, je dois répéter le calcul dans le test, ce qui ne me semble pas correct. Quelle est la meilleure façon de définir la date sur une valeur connue dans le test afin que je puisse tester que le résultat est une valeur connue?

Craig.Nicol
la source

Réponses:

157

Ma préférence est que les classes qui utilisent le temps reposent en fait sur une interface, telle que

interface IClock
{
    DateTime Now { get; } 
}

Avec une mise en œuvre concrète

class SystemClock: IClock
{
     DateTime Now { get { return DateTime.Now; } }
}

Ensuite, si vous le souhaitez, vous pouvez fournir tout autre type d'horloge que vous souhaitez pour le test, tel que

class StaticClock: IClock
{
     DateTime Now { get { return new DateTime(2008, 09, 3, 9, 6, 13); } }
}

Il peut y avoir une surcharge à fournir l'horloge à la classe qui en dépend, mais cela pourrait être géré par n'importe quel nombre de solutions d'injection de dépendances (en utilisant un conteneur d'inversion de contrôle, une ancienne injection de constructeur / setter, ou même un modèle de passerelle statique ).

D'autres mécanismes de livraison d'un objet ou d'une méthode qui fournit les heures souhaitées fonctionnent également, mais je pense que l'essentiel est d'éviter de réinitialiser l'horloge système, car cela ne fera qu'introduire de la douleur à d'autres niveaux.

De plus, l'utiliser DateTime.Nowet l'inclure dans vos calculs ne vous semble pas juste - cela vous prive de la possibilité de tester des moments particuliers, par exemple si vous découvrez un bogue qui ne se produit que près d'une limite de minuit ou le mardi. L'utilisation de l'heure actuelle ne vous permettra pas de tester ces scénarios. Ou du moins pas quand vous le souhaitez.

Blair Conrad
la source
2
Nous l'avons en fait formalisé dans l'une des extensions xUnit.net. Nous avons une classe Clock que vous utilisez comme statique plutôt que DateTime, et vous pouvez "geler" et "dégeler" l'horloge, y compris à des dates spécifiques. Voir is.gd/3xds et is.gd/3xdu
Brad Wilson
2
Il convient également de noter que lorsque vous souhaitez remplacer votre méthode d'horloge système - cela se produira, par exemple, lorsque vous utilisez une horloge globale dans une entreprise avec des succursales dans des fuseaux horaires largement dispersés - cette méthode vous donne une précieuse liberté au niveau de l'entreprise pour modifier le sens de «maintenant».
Mike Burton
1
Cette méthode fonctionne très bien pour moi, en plus d'utiliser un Framework d'injection de dépendances pour accéder à l'instance IClock.
Wilka
8
Très bonne réponse. Je voulais juste ajouter que dans presque tous les cas, il UtcNowdevrait être utilisé puis ajusté de manière appropriée en fonction des préoccupations du code, par exemple la logique métier, l'interface utilisateur, etc. Heure UTC.
Adam Ralph
@BradWilson Ces liens sont maintenant rompus. Je ne pouvais pas non plus les récupérer sur WayBack.
55

Ayende Rahien utilise une méthode statique assez simple ...

public static class SystemTime
{
    public static Func<DateTime> Now = () => DateTime.Now;
}
Anthony Mastrean
la source
1
Il semble dangereux de faire du stub / mock point une variable globale publique (variable statique de classe). Ne serait-il pas préférable de l'étendre uniquement au système testé - par exemple, en en faisant un membre statique privé de la classe testée?
Aaron
1
C'est une question de style. C'est la moindre des choses que vous puissiez faire pour obtenir une heure système modifiable par un test unitaire.
Anthony Mastrean
3
IMHO, il est préférable d'utiliser l'interface au lieu de singletons statiques globaux. Considérez le scénario suivant: Le lanceur de tests est efficace et exécute autant de tests que possible en parallèle, un test passe à l'heure donnée en X, l'autre en Y. Nous avons maintenant un conflit et ces deux tests vont faire basculer les échecs. Si nous utilisons des interfaces, chaque test se moque de l'interface en fonction de ses besoins, chaque test est maintenant isolé des autres tests. HTH.
ShloEmi
17

Je pense que créer une classe d'horloge séparée pour quelque chose de simple comme obtenir la date actuelle est un peu exagéré.

Vous pouvez transmettre la date du jour en tant que paramètre afin de pouvoir saisir une date différente dans le test. Cela présente l'avantage supplémentaire de rendre votre code plus flexible.

Mendelt
la source
J'ai attribué +1 à la vôtre et à celle de Blair, même si elles sont toutes deux opposées. Je pense que les deux approches sont valables. Votre approche J'utiliserais probablement un projet qui n'utilise pas quelque chose comme Unity.
RichardOD
1
Oui, il est possible d'ajouter un paramètre pour "maintenant". Toutefois, dans certains cas, cela vous oblige à exposer un paramètre que vous ne souhaiteriez normalement pas exposer. Disons, par exemple, que vous avez une méthode qui calcule les jours entre une date et maintenant, alors vous ne voulez pas exposer "maintenant" comme paramètre car cela permet de manipuler votre résultat. S'il s'agit de votre propre code, ajoutez "maintenant" comme paramètre, mais si vous travaillez en équipe, vous ne savez jamais à quoi les autres développeurs utiliseront votre code, donc si "maintenant" est une partie critique de votre code, vous devez protégez-le de toute manipulation ou utilisation abusive.
Stitch10925
17

L'utilisation de Microsoft Fakes pour créer un shim est un moyen très simple de le faire. Supposons que j'ai eu le cours suivant:

public class MyClass
{
    public string WhatsTheTime()
    {
        return DateTime.Now.ToString();
    }

}

Dans Visual Studio 2012, vous pouvez ajouter un assemblage Fakes à votre projet de test en cliquant avec le bouton droit sur l'assemblage pour lequel vous souhaitez créer Fakes / Shims et en sélectionnant «Ajouter un assemblage Fakes»

Ajout de faux assemblages

Enfin, voici à quoi ressemblerait la classe de test:

using System;
using ConsoleApplication11;
using Microsoft.QualityTools.Testing.Fakes;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DateTimeTest
{
[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestWhatsTheTime()
    {

        using(ShimsContext.Create()){

            //Arrange
            System.Fakes.ShimDateTime.NowGet =
            () =>
            { return new DateTime(2010, 1, 1); };

            var myClass = new MyClass();

            //Act
            var timeString = myClass.WhatsTheTime();

            //Assert
            Assert.AreEqual("1/1/2010 12:00:00 AM",timeString);

        }
    }
}
}
mmilleruva
la source
1
C'était exactement ce que je cherchais. Merci! BTW, fonctionne de la même manière dans VS 2013.
Douglas Ludlow
Ou ces jours-ci, VS édition Entreprise 2015. Ce qui est dommage, pour une telle pratique exemplaire.
RJB
12

La clé du succès des tests unitaires est le découplage . Vous devez séparer votre code intéressant de ses dépendances externes, afin qu'il puisse être testé isolément. (Heureusement, le développement piloté par les tests produit du code découplé.)

Dans ce cas, votre externe est le DateTime actuel.

Mon conseil ici est d'extraire la logique qui traite le DateTime vers une nouvelle méthode ou classe ou tout ce qui a du sens dans votre cas, et de passer le DateTime. Maintenant, votre test unitaire peut passer un DateTime arbitraire, pour produire des résultats prévisibles.

Jay Bazuzi
la source
10

Un autre utilisant Microsoft Moles ( Isolation Framework for .NET ).

MDateTime.NowGet = () => new DateTime(2000, 1, 1);

Moles permet de remplacer n'importe quelle méthode .NET par un délégué. Moles prend en charge les méthodes statiques ou non virtuelles. Moles s'appuie sur le profileur de Pex.

João Angelo
la source
C'est beau, mais cela nécessite Visual Studio 2010! :-(
Pandincus
Cela fonctionne également bien dans VS 2008. Cela fonctionne mieux avec MSTest cependant. Vous pouvez utiliser NUnit, mais je pense que vous devez exécuter vos tests avec un testeur spécial.
Torbjørn
J'éviterais d'utiliser Moles (aka Microsoft Fakes) lorsque cela est possible. Idéalement, il ne doit être utilisé que pour le code hérité qui n'est pas déjà testable via l'injection de dépendances.
brianpeiris
1
@brianpeiris, quels sont les inconvénients de l'utilisation de Microsoft Fakes?
Ray Cheng
Vous ne pouvez pas toujours DI contenu tiers, donc Fakes est parfaitement acceptable pour un test unitaire si votre code appelle une API tierce que vous ne souhaitez pas instancier pour un test unitaire. J'accepte d'éviter d'utiliser Fakes / Moles pour votre propre code, mais c'est parfaitement acceptable à d'autres fins.
ChrisCW
5

Je suggère d'utiliser le modèle IDisposable:

[Test] 
public void CreateName_AddsCurrentTimeAtEnd() 
{
    using (Clock.NowIs(new DateTime(2010, 12, 31, 23, 59, 00)))
    {
        string name = new ReportNameService().CreateName(...);
        Assert.AreEqual("name 2010-12-31 23:59:00", name);
    } 
}

En détail décrit ici: http://www.lesnikowski.com/blog/index.php/testing-datetime-now/

Pawel Lesnikowski
la source
2

Vous pouvez injecter la classe (mieux: méthode / délégué ) que vous utilisez DateTime.Nowdans la classe testée. Avoir DateTime.Nowune valeur par défaut et ne la définir dans le test que sur une méthode factice qui renvoie une valeur constante.

EDIT: Ce que Blair Conrad a dit (il a du code à regarder). Sauf que j'ai tendance à préférer les délégués pour cela, car ils n'encombrent pas votre hiérarchie de classe avec des trucs comme IClock...

Daren Thomas
la source
1

J'ai fait face à cette situation si souvent, que j'ai créé un simple nuget qui expose la propriété Now via l'interface.

public interface IDateTimeTools
{
    DateTime Now { get; }
}

La mise en œuvre est bien sûr très simple

public class DateTimeTools : IDateTimeTools
{
    public DateTime Now => DateTime.Now;
}

Donc, après avoir ajouté nuget à mon projet, je peux l'utiliser dans les tests unitaires

entrez la description de l'image ici

Vous pouvez installer le module directement à partir du gestionnaire de packages GUI Nuget ou en utilisant la commande:

Install-Package -Id DateTimePT -ProjectName Project

Et le code pour le Nuget est ici .

L'exemple d'utilisation avec l'Autofac peut être trouvé ici .

Pawel Wujczyk
la source
-11

Avez-vous envisagé d'utiliser la compilation conditionnelle pour contrôler ce qui se passe pendant le débogage / le déploiement?

par exemple

DateTime date;
#if DEBUG
  date = new DateTime(2008, 09, 04);
#else
  date = DateTime.Now;
#endif

À défaut, vous voulez exposer la propriété afin de pouvoir la manipuler, tout cela fait partie du défi d'écrire du code testable , ce que je lutte moi-même actuellement: D

Éditer

Une grande partie de moi préférerait l'approche de Blair . Cela vous permet de «connecter à chaud» des parties du code pour faciliter les tests. Tout suit le principe de conception: encapsuler ce qui varie le code de test n'est pas différent du code de production, c'est juste que personne ne le voit jamais de l'extérieur.

La création et l'interface peuvent sembler beaucoup de travail pour cet exemple (c'est pourquoi j'ai opté pour la compilation conditionnelle).

Rob Cooper
la source
wow vous avez été durement touché par cette réponse. c'est ce que j'ai fait, bien que dans un seul endroit, où j'appelle des méthodes qui remplacent DateTimeles Nowet Todayetc.
Dave Cousineau