“Mocker” une dépendance statique

Les objets statiques sont souvent utilisés pour mutualiser rapidement l’implémentation d’un comportement ou plus rarement pour partager des instances d’objets entre plusieurs classes.
L’utilisation d’objets statiques peut être un choix d’architecture maitrisé. Dans ce cas, on a la possibilité de modifier l’implémentation des objets statiques ainsi que des objets consommateurs.

Dans d’autres cas, l’utilisation d’objets statiques peut être imposée par l’implémentation d’une bibliothèque par exemple. C’est le plus souvent dans ce cas qu’il est plus difficile de mocker les objets statiques pour tester l’implémentation des classes consommatrices.

Cet article présente quelques méthodes pour mocker des objets statiques:

  • Dans le cas où l’implémentation de l’objet statique est maitrisé: en injectant une dépendance,
  • Dans le cas où l’implémentaiton est contrainte.

On considère l’exemple suivant permettant de calculer l’âge en fonction de la date actuelle:

public class AgeCalculator 
{ 
    public int GetAge(DateTime dateOfBirth) 
    { 
        DateTime now = DateTime.Now; 
        if (dateOfBirth > now) 
            throw new InvalidOperationException(
               "Date of birth shall be before current date."); 
     
        return now.Year - dateOfBirth.Year; 
    } 
}

Cette implémentation très simpliste ne prends pas en compte les mois et donc peut s’avèrer faux toutefois si on souhaite tester AgeCalculator.GetAge(), on pourrait écrire le test suivant:

[Test] 
public void Age_Shall_Be_14_When_DateOfBirth_Is_In_2002() 
{ 
    DateTime fixedDateOfBirth = new DateTime(2002, 06, 06); 
 
    var ageCalculator = new AgeCalculator(); 
    int age = ageCalculator.GetAge(fixedDateOfBirth); 
     
    Assert.AreEqual(14, age); 
}

Ce test fonctionnera jusqu’au 1 janvier 2017. A partir de cette date, il sera faux. On peut donc modifier le test pour être moins dépendant de la date en cours.

[Test] 
public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago() 
{ 
    DateTime now = DateTime.Now; 
    DateTime dateOfBirth = new DateTime(now.Year - 14, now.Month, now.Day); 
 
    var ageCalculator = new AgeCalculator(); 
    int age = ageCalculator.GetAge(dateOfBirth); 
 
    Assert.AreEqual(14, age); 
}

Ce test fonctionnera tout le temps mais peut échouer à certaines occasions. Par exemple, si le test est exécuté le 31 décembre 2016 à 23h59min59sec999ms. La date de naissance 14 ans plus tôt sera le 31 décembre 2002 à 23h59min59sec999ms. Toutefois pendant l’exécution de AgeCalculator.GetAge(), l’horloge continue d’avancer et au moment d’exécuter System.DateTime.Now dans la fonction AgeCalculator.GetAge(), la date actuelle devient: 1er janvier 2017 à 0h00min00sec001ms. L’âge calculé devient 15 et le test échoue.

Pour que le test réussisse toujours et ne soit plus dépendant de la date en cours, il faudrait pouvoir maitriser la date renvoyée par la propriété statique DateTime.Now. Or DateTime est une classe du framework.

Injecter une dépendance

La première solution consiste à injecter une dépendance dans la classe consommatrice plutôt que de permettre d’utiliser directement la propriété statique. L’injection de la dépendance permettra, ainsi, de casser la dépendance vers la propriété statique.

L’implémentation de AgeCalculator devient:

public class AgeCalculator 
{ 
    private ICurrentDateHandler currentDateHandler; 
 
    public AgeCalculator(ICurrentDateHandler currentDateHandler) 
    { 
        this.currentDateHandler = currentDateHandler; 
    } 
 
    public int GetAge(DateTime dateOfBirth) 
    { 
        DateTime now = this.currentDateHandler.GetCurrentDate(); 
        if (dateOfBirth > now) 
            throw new InvalidOperationException(
               "Date of birth shall be before current date."); 
     
        return now.Year - dateOfBirth.Year; 
    } 
}

Avec:

public interface ICurrentDateHandler 
{ 
    DateTime GetCurrentDate(); 
}

Et:

public class CurrentDateHandler : ICurrentDateHandler 
{ 
    public DateTime GetCurrentDate() 
    { 
        return DateTime.Now; 
    } 
}

On casse le couplage entre AgeCalculator et System.DateTime en déportant la dépendance à System.DateTime dans une autre classe:
Cette implémentation est plus en accord avec le principe SOLID (http://cdiese.fr/principe-de-developpement-oriente-objet-solid/) puisqu’elle sépare les responsabilités.
Elle facilite les tests unitaires puisqu’on peut maintenant maitrisé la date actuelle fournie à AgeCalculator.

On peut facilement mocker ICurrentDateHandler, par exemple, avec Moq:

using Moq; 
// ...

DateTime expectedCurrentDate = new DateTime(2016, 6, 6); 
 
Mock<ICurrentDateHandler> currentDateHandlerMock = new Mock<ICurrentDateHandler>(); 
currentDateHandlerMock.SetUp(h => h.GetCurrentDate()).Returns(expectedCurrentDate);

Le test devient:

[Test]  
public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago()  
{  
    DateTime expectedCurrentDate = new DateTime(2016, 6, 6); 
 
    Mock<ICurrentDateHandler> currentDateHandlerMock = new Mock<ICurrentDateHandler>(); 
    currentDateHandlerMock.SetUp(h => h.GetCurrentDate()).Returns(expectedCurrentDate); 
 
    DateTime dateOfBirth = new DateTime(expectedCurrentDate.Year - 14,  
        expectedCurrentDate.Month, expectedCurrentDate.Day);  
  
    var ageCalculator = new AgeCalculator(currentDateHandlerMock.Object);  
    int age = ageCalculator.GetAge(dateOfBirth);  
  
    Assert.AreEqual(14, age);  
}

Le test ne dépend plus de la date actuelle et réussit toujours.

Utiliser une Factory

L’inconvénient de l’exemple précédent est que l’injection de ICurrentDateHandler se fait dans le constructeur de AgeCalculator ce qui contraint l’extensibilité de la classe AgeCalculator.

Si on doit créer un objet ICurrentDateHandler suivant certains critères connus au moment de l’exécution de AgeCalculator.GetAge(), on peut passer par l’intermédiaire d’une Factory. Cette Factory est injectée par le constructeur toutefois, elle permet de créer l’objet ICurrentDateHandler suivant une logique implémentée directement dans la Factory. La création peut aussi être exécutée au moment de l’appel à AgeCalculator.GetAge() et non à la construction de AgeCalculator.

Par exemple, si on doit introduire une notion de fuseaux horaires sans modifier la signature de AgeCalculator.GetAge(), on peut le faire par l’intermédiaire de la Factory:

public class DateHandlerFactory : IDateHandlerFactory 
{ 
    private TimeZoneInfo sourceTimeZone; 
    private TimeZoneInfo destinationTimeZone; 
 
    public CurrentDateHandlerFactory(TimeZoneInfo sourceTimeZone,  
        TimeZoneInfo destinationTimeZone) 
    { 
         this.sourceTimeZone = sourceTimeZone; 
         this.destinationTimeZone = destinationTimeZone; 
    } 
 
    public ICurrentDateHandler GetCurrentDateHandler() 
    { 
        return new CurrentDateHandler(this.sourceTimeZone, this.destinationTimeZone); 
    } 
}

Avec:

public interface IDateHandlerFactory 
{ 
    ICurrentDateHandler GetCurrentDateHandler(); 
}

urrentDateHandler devient:

public class CurrentDateHandler : ICurrentDateHandler 
{ 
    private TimeZoneInfo sourceTimeZone; 
    private TimeZoneInfo destinationTimeZone; 
 
    public CurrentDateHandlerFactory(TimeZoneInfo sourceTimeZone,  
        TimeZoneInfo destinationTimeZone) 
    { 
         this.sourceTimeZone = sourceTimeZone; 
         this.destinationTimeZone = destinationTimeZone; 
    } 
 
    public DateTime GetCurrentDate() 
    { 
        return TimeZoneInfo.ConvertTime(DateTime.Now, this.sourceTimeZone,  
            this.destinationTime); 
    } 
}

De même AgeCalculator prend en compte la Factory toutefois son implémentation ne contient aucune modifications relatives au fuseau horaire:

public class AgeCalculator 
{ 
    private IDateHandlerFactory dateHandlerFactory; 
 
    public AgeCalculator(IDateHandlerFactory dateHandlerFactory)  
    { 
        this.dateHandlerFactory = dateHandlerFactory; 
    } 
 
    public int GetAge(DateTime dateOfBirth) 
    { 
        var currentDateHandler = this.dateHandlerFactory.GetCurrentDateHandler(); 
 
        DateTime now = currentDateHandler.GetCurrentDate(); 
        if (dateOfBirth > now) 
            throw new InvalidOperationException(
                "Date of birth shall be before current date."); 
     
        return now.Year - dateOfBirth.Year; 
    } 
}

On peut toujours mocker ICurrentDateHandler ainsi que la Factory IDateHandlerFactory pour maitriser la date actuelle dans le test:

[Test]   
public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago()   
{   
    DateTime expectedCurrentDate = new DateTime(2016, 6, 6);  
  
    Mock<ICurrentDateHandler> currentDateHandlerMock = new Mock<ICurrentDateHandler>();  
    currentDateHandlerMock.SetUp(h => h.GetCurrentDate()).Returns(expectedCurrentDate);  
 
    Mock<IDateHandlerFactory> dateHandlerFactoryMock = new Mock<IDateHandlerFactory>();  
    dateHandlerFactoryMock.SetUp(f => f.GetCurrentDateHandler()) 
        .Returns(currentDateHandlerMock.Object); 
  
    DateTime dateOfBirth = new DateTime(expectedCurrentDate.Year - 14,   
        expectedCurrentDate.Month, expectedCurrentDate.Day);   
   
    var ageCalculator = new AgeCalculator(dateHandlerFactoryMock.Object);   
    int age = ageCalculator.GetAge(dateOfBirth);   
   
    Assert.AreEqual(14, age);   
}

Utiliser une classe statique Proxy

L’inconvénient majeur des méthodes précédentes est qu’elles imposent de modifier en profondeur la classe AgeCalculator car:

  • Il faut modifier le constructeur pour permettre l’injection de la dépendance,
  • Il faut modifier la fonction AgeCalculator.GetAge() pour qu’elle utilise ICurrentDateHandler.GetCurrentDate() plutôt que DateTime.Now.

On pourrait explorer d’autres méthodes pour injecter la dépendance:

  • Injecter la dépendance par appel d’une méthode pour éviter la modification du constructeur, par exemple SetCurrentDateHandler():
    public class AgeCalculator 
    { 
        private ICurrentDateHandler currentDateHandler; 
     
        public AgeCalculator() {} 
     
        public void SetCurrentDateHandler(ICurrentDateHandler currentDateHandler) 
        { 
            this.currentDateHandler = currentDateHandler; 
        } 
     
        // ... 
    }
    
  • Injection par un accesseur:
    public class AgeCalculator 
    { 
        public AgeCalculator() {} 
     
        public ICurrentDateHandler CurrentDateHandler { get; set; }  
     
        public int GetAge(DateTime dateOfBirth) 
        { 
            DateTime now = this.CurrentDateHandler.GetCurrentDate(); 
            if (dateOfBirth > now) 
                throw new InvalidOperationException(
                    "Date of birth shall be before current date."); 
         
            return now.Year - dateOfBirth.Year; 
        } 
    }
    
  • Injecter directement dans la fonction AgeCalculator.GetAge():
    public class AgeCalculator 
    { 
        public AgeCalculator() {} 
     
        public int GetAge(ICurrentDateHandler currentDateHandler, DateTime dateOfBirth) 
        { 
            DateTime now = currentDateHandler.GetCurrentDate(); 
            if (dateOfBirth > now) 
                throw new InvalidOperationException(
                    "Date of birth shall be before current date."); 
         
            return now.Year - dateOfBirth.Year; 
        } 
    }
    

Ces solutions sont très mauvaises car elles imposent que l’appelant de AgeCalculator.GetAge() crée une instance d’un objet ICurrentDateHandler et qu’il l’injecte au bon moment dans AgeCalculator. On va donc explorer une autre possibilité qui consiste à continuer à utiliser une classe statique dans la fonction AgeCalculator.GetAge().

L’intérêt de cette approche est:

  • Elle ne modifie pas le constructeur de AgeCalculator,
  • Elle ne nécessite pas une injection de ICurrentDateHandler,
  • La modification n’implique que la fonction AgeCalculator.GetAge().

Ainsi si on considère la classe suivante:

public static class DateHandler 
{ 
    public static Func<DateTime> Now = () => DateTime.Now; 
}

On peut l’utiliser directement dans AgeCalculator sans injection:

public class AgeCalculator 
{ 
    public AgeCalculator() {} 
 
    public int GetAge(DateTime dateOfBirth) 
    { 
        DateTime now = DateHandler.Now; 
        if (dateOfBirth > now) 
            throw new InvalidOperationException(
                "Date of birth shall be before current date."); 
     
        return now.Year - dateOfBirth.Year; 
    } 
}

On peut toujours maitriser la date actuelle dans les tests. Notre test devient:

[Test]  
public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago()  
{  
    DateTime expectedCurrentDate = new DateTime(2016, 6, 6); 
    DateHandler.Now = () => expectedCurrentDate; 
 
    DateTime dateOfBirth = new DateTime(expectedCurrentDate.Year - 14,  
        expectedCurrentDate.Month, expectedCurrentDate.Day);  
  
    var ageCalculator = new AgeCalculator();  
    int age = ageCalculator.GetAge(dateOfBirth);  
  
    Assert.AreEqual(14, age);  
}

Le constructeur de AgeCalculator et la signature de AgeCalculator.GetAge() ne sont pas modifiés.

Smocks

Une dernière possibilité consiste à ne pas modifier du tout la classe AgeCalculator et continuer à utiliser la propriété statique DateTime.Now. On peut contrôler la valeur de DateTime.Now dans les tests en utilisant des bibliothèques comme Smocks.

Le gros intérêt de ce type de bibliothèque est de modifier directement les valeurs renvoyées par une propriété ou des fonctions statiques, par exemple pour le test précédent:

[Test]  
public void Age_Shall_Be_14_When_DateOfBirth_Is_14_Years_Ago()  
{  
    Smock.Run(context => 
    { 
        DateTime expectedCurrentDate = new DateTime(2016, 6, 6); 
 
        context.Setup(() => DateTime.Now).Returns(expectedCurrentDate); 
 
       DateTime dateOfBirth = new DateTime(expectedCurrentDate.Year - 14,  
            expectedCurrentDate.Month, expectedCurrentDate.Day);  
  
        var ageCalculator = new AgeCalculator();  
        int age = ageCalculator.GetAge(dateOfBirth);  
  
        Assert.AreEqual(14, age); 
     }); 
}

Le code de AgeCalculator n’est pas modifié:

public class AgeCalculator 
{ 
    public AgeCalculator() {} 
 
    public int GetAge(ICurrentDateHandler currentDateHandler, DateTime dateOfBirth) 
    { 
        DateTime now = DateTime.Now; 
        if (dateOfBirth > now) 
            throw new InvalidOperationException(
                "Date of birth shall be before current date."); 
     
        return now.Year - dateOfBirth.Year; 
    } 
}

Smocks est disponible sur Nuget: https://www.nuget.org/packages/Smocks/.

Microsoft Fakes

Une autre alternative est d’utiliser Microsoft Fakes.
L’inconvénient de Microsoft Fakes est qu’il n’est disponible que dans Visual Studio Enterprise ce qui en fait un produit cher.
Le projet d’origine Moles était gratuit toutefois depuis Visual Studio 2010, il n’a pas évolué au profit de Microsoft Fakes.

Leave a Reply