Partager via


Utilisation de shims pour isoler votre application des autres assemblys pour des tests unitaires

Les types Shim (cales) sont l'une de deux technologies qu'utilise le framework Microsoft Fakes pour vous permettre de facilement isoler de l'environnement des composants testés.Les shims détournent les appels aux méthodes spécifiques de code que vous écrivez dans le cadre de votre test.De nombreuses méthodes retournent des résultats différents selon les conditions externes, mais un shim est sous le contrôle de votre test et peut retourner des résultats cohérents pour chaque appel.Cela facilite l'écriture de vos tests.

Utilisez les shims pour isoler le code des assemblys qui ne font pas partie de votre solution.sPour isoler les composants de votre solution entre eux, nous vous recommandons d'utiliser les stubs.

Pour obtenir une aide de présentation et de démarrage rapide, consultez Isolation du code sous test avec Microsoft Fakes

Conditions requises

  • Visual Studio Ultimate

Consultez Vidéo (1h16) : Le code non testable avec Fakes dans Visual Studio 2012

Dans cette rubrique

Voici ce que vous allez apprendre dans cette rubrique :

Exemple : le bogue de l'an 2000

Procédure d'utilisation des shims

  • Ajouter les assemblys Fakes

  • Utiliser ShimsContext

  • Ecrire des test avec des shims

Shims pour différents types de méthodes

Modifier le comportement par défaut

Détecter les accès d'environnement

l'accès concurrentiel ;

Appeler la méthode d'origine de la méthode shim

Limitations

Exemple : le bogue de l'an 2000

Considérons une méthode qui lève une exception le 1er janvier 2000 :

// code under test
public static class Y2KChecker {
    public static void Check() {
        if (DateTime.Now == new DateTime(2000, 1, 1))
            throw new ApplicationException("y2kbug!");
    }
}

Tester cette méthode est particulièrement problématique car le programme dépend de DateTime.Now, une méthode qui dépend de l'horloge de l'ordinateur, une méthode dépendante de l'environnement et non déterministe.En outre, DateTime.Now est une propriété statique, un stub ne peut donc pas être utilisé ici.Ce problème est symptomatique du problème d'isolement dans les tests unitaire : les programmes qui appellent directement les APIs de base de données, communiquent avec les services Web et sont donc durs à tester unitairement car leur logique dépend de l'environnement.

C'est dans ce cas, que des types « shim » doivent être utilisés.Les types shim fournissent un mécanisme pour détourner toute méthode .NET par un délégué défini par l'utilisateur.Les types shim sont générés par le code par le générateur de faux (Fake), et utilisent les délégués, que nous appelons types « shim », pour spécifier les nouvelles implémentations des méthodes.

Le test suivant montre comment utiliser le type shim, ShimDateTime, pour fournir une implémentation personnalisée de DateTime.Now :

//unit test code
// create a ShimsContext cleans up shims 
using (ShimsContext.Create()
    // hook delegate to the shim method to redirect DateTime.Now
    // to return January 1st of 2000
    ShimDateTime.NowGet = () => new DateTime(2000, 1, 1);
    Y2KChecker.Check();
}

Procédure d'utilisation des shims

Ajouter les assemblys Fakes

  1. Dans l'Explorateur de solutions, développez vos References du projet de test unitaire

    • Si vous travaillez en Visual Basic, vous devez sélectionner Afficher tous les fichiers dans la barre d'outils de l'explorateur de solutions, pour consulter la liste de références.
  2. Sélectionnez l'assembly contenant les définitions de classes pour lesquels vous souhaitez créer des shims.Par exemple, si vous souhaitez effectuer un shim pour datetime, sélectionnez System.dll

  3. Dans le menu contextuel, sélectionnez Add Fakes Assembly.

Utiliser ShimsContext

Lors de l'utilisation de types shim dans un framework de test unitaire, vous devez encapsuler le code de test dans un ShimsContext pour contrôler la durée de vie de vos shims.Si nous ne faisions pas cela, vos shims dureraient jusqu'à ce que l'AppDomain ne s'arrête.La façon la plus facile de créer un ShimsContext est à l'aide de la méthode statique Create() comme montré dans le code suivant :

//unit test code
[Test]
public void Y2kCheckerTest() {
  using(ShimsContext.Create()) {
    ...
  } // clear all shims
}

Il est essentiel de disposer correctement chaque contexte de chaque shim.En règle général, appelez toujours ShimsContext.Create à l'intérieur d'une instruction using pour garantir le nettoyage correct des shims inscrits.Par exemple, vous pouvez stocker un shim pour une méthode de test qui remplace la méthode DateTime.Now par un délégué qui retourne toujours le premier janvier 2000.Si vous oubliez de nettoyer le shim stockée dans la méthode de test, le reste de la série de tests retourne toujours le premier janvier 2000 comme valeur de DateTime.Now.Cela peut être surprenant et déroutant.

Écrire un test avec les shims

Dans votre code de test, insérez un détour de la méthode que vous souhaitez truquer.Par exemple :

[TestClass]
public class TestClass1
{ 
        [TestMethod]
        public void TestCurrentYear()
        {
            int fixedYear = 2000;

            using (ShimsContext.Create())
            {
              // Arrange:
                // Detour DateTime.Now to return a fixed date:
                System.Fakes.ShimDateTime.NowGet = 
                () =>
                { return new DateTime(fixedYear, 1, 1); };

                // Instantiate the component under test:
                var componentUnderTest = new MyComponent();

              // Act:
                int year = componentUnderTest.GetTheCurrentYear();

              // Assert: 
                // This will always be true if the component is working:
                Assert.AreEqual(fixedYear, year);
            }
        }
}
<TestClass()> _
Public Class TestClass1
    <TestMethod()> _
    Public Sub TestCurrentYear()
        Using s = Microsoft.QualityTools.Testing.Fakes.ShimsContext.Create()
            Dim fixedYear As Integer = 2000
            ' Arrange:
            ' Detour DateTime.Now to return a fixed date:
            System.Fakes.ShimDateTime.NowGet = _
                Function() As DateTime
                    Return New DateTime(fixedYear, 1, 1)
                End Function

            ' Instantiate the component under test:
            Dim componentUnderTest = New MyComponent()
            ' Act:
            Dim year As Integer = componentUnderTest.GetTheCurrentYear
            ' Assert: 
            ' This will always be true if the component is working:
            Assert.AreEqual(fixedYear, year)
        End Using
    End Sub
End Class

Les noms de classe de shim sont obtenus en préfixant Fakes.Shim au nom de type d'origine.

Les shims s'exécutent en insérant des détours dans le code de l'application de test.Quelque soit l'endroit où l'appel à la méthode d'origine se produit, le système Fakes effectue un détour afin qu'au lieu d'appeler la méthode true, le code de shim est appelé.

Notez que les détours sont créés et supprimés au moment de l'exécution.Vous devez toujours créer un détour dans la durée de vie de ShimsContext.Lorsqu'elle est préparée, tous les shims que vous avez créés lorsqu'ils étaient actifs sont supprimés.La meilleure méthode consiste à l'effectuer à l'intérieur d'une instruction using.

Vous pouvez obtenir une erreur de build indiquant que l'espace de noms (false) n'existe pas.Cette erreur se produit parfois lorsqu'il existe d'autres erreurs de compilation.Corrigez les autres erreurs et elle disparaîtra.

Shims pour différents types de méthodes

Les types shim vous permettent de remplacer toute méthode .NET, y compris les méthodes statiques ou méthodes non-virtuelles, par vos propres délégués.

Méthodes statiques

Les propriétés pour attacher des shims aux méthodes statiques sont placées dans un type shim.Chaque propriété n'a qu'un accesseur Set qui peut être utilisé pour attacher un délégué à la méthode cible.Par exemple, à partir d'une classe MyClass avec une méthode statique MyMethod:

//code under test
public static class MyClass {
    public static int MyMethod() {
        ...
    }
}

Nous pouvons joindre un shim à MyMethod qui retourne toujours 5 :

// unit test code
ShimMyClass.MyMethod = () =>5;

Méthodes d'instance (pour toutes les instances)

De façon similaires qu'aux méthodes statiques, des méthodes d'instance peuvent être calées (shimmed) pour toutes les instances.Les propriétés pour attacher ces shims sont placées dans un type imbriqué nommé AllInstances pour éviter toute confusion.Par exemple, à partir d'une classe MyClass avec une méthode d'instance MyMethod:

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

Vous pouvez joindre un shim à MyMethod qui retourne toujours 5, indépendamment de l'instance :

// unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;

La structure du type généré ShimMyClass ressemble au code suivant :

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public static class AllInstances {
        public static Func<MyClass, int>MyMethod {
            set {
                ...
            }
        }
    }
}

Notez que les Faux passe l'instance d'exécution comme premier argument du délégué dans ce cas.

Méthodes d'instance (pour une instance d'exécution)

Les méthodes d'instance peuvent également être calées (shimmed) par des délégués, selon le récepteur de l'appel.Cela permet à la même méthode d'instance d'avoir des comportements différents par instance du type.Les propriétés pour installer ces shims sont des méthodes d'instance du type shim lui-même.Chaque type shim instancié est également associé à une instance brut d'un type calé (shimmed).

Par exemple, à partir d'une classe MyClass avec une méthode d'instance MyMethod:

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

Nous pouvons installer deux types shim de MyMethod tels que le premier retourne toujours 5 et le deuxième retourne toujours 10 :

// unit test code
var myClass1 = new ShimMyClass()
{
    MyMethod = () => 5
};
var myClass2 = new ShimMyClass { MyMethod = () => 10 };

La structure du type généré ShimMyClass ressemble au code suivant :

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public Func<int> MyMethod {
        set {
            ...
        }
    }
    public MyClass Instance {
        get {
            ...
        }
    }
}

L'instance du type shim réel est accessible via la propriété de l'instance :

// unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;

Le type shim a également une conversion implicite au type calé (shimmed), vous pouvez généralement simplement utiliser le type shim comme :

// unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // implicit cast retrieves the runtime
                         // instance

Constructeurs

Les constructeurs peuvent également être calés (shimmed) pour attacher des types shim à de futures objets.Chaque constructeur est exposé comme un constructeur de méthode statique dans le type du shim.Par exemple, à partir d'une classe MyClass avec un constructeur prenant un entier :

// code under test
public class MyClass {
    public MyClass(int value) {
        this.Value = value;
    }
    ...
}

Nous installons le type shim du constructeur afin que chaque instance ultérieure retourne -5 lorsque l'accesseur Get est appelé, indépendamment de la valeur dans le constructeur :

// unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
    var shim = new ShimMyClass(@this) {
        ValueGet = () => -5
    };
};

Notez que chaque type shim expose deux constructeurs.Le constructeur par défaut doit être utilisé lorsqu'une nouvelle instance est nécessaire, tandis que le constructeur prenant une instance calée (shimmed) comme argument doit être utilisé dans le constructeur shim uniquement :

// unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }

La structure du type généré ShimMyClass ressemble au code suivant :

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
    public static Action<MyClass, int> ConstructorInt32 {
        set {
            ...
        }
    }

    public ShimMyClass() { }
    public ShimMyClass(MyClass instance) : base(instance) { }
    ...
}

Membres de base

Les propriétés du shim des membres de base sont accessibles en créant un shim pour le type de base et en passant l'instance enfant comme paramètre au constructeur de la classe de base du shim.

Par exemple, à partir d'une classe MyBase avec une méthode d'instance MyMethod et un sous-type MyChild:

public abstract class MyBase {
    public int MyMethod() {
        ...
    }
}

public class MyChild : MyBase {
}

Nous pouvons installer un shim de MyBase en créant un nouveau shim de ShimMyBase :

// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };

Notez que l'enfant du type shim est converti implicitement en instance enfant une fois passé comme paramètre au constructeur de base du shim.

La structure du type généré ShimMyChild et de ShimMyBase ressemble au code suivant :

// Fakes generated code
public class ShimMyChild : ShimBase<MyChild> {
    public ShimMyChild() { }
    public ShimMyChild(Child child)
        : base(child) { }
}
public class ShimMyBase : ShimBase<MyBase> {
    public ShimMyBase(Base target) { }
    public Func<int> MyMethod
    { set { ... } }
}

Constructeurs statiques

Les types shim exposent une méthode statique StaticConstructor pour mettre en place un shim sur le constructeur statique d'un type.Étant donné que les constructeurs statiques sont exécutés une seule fois, vous devez vérifier que le shim est configurée avant que tout membre du type soit accéder.

Finaliseurs

Les finaliseurs ne sont pas pris en charge dans Fakes.

Méthodes privées

Le générateur de code de Fakes crée des propriétés shim pour les méthodes privées qui possèdent uniquement des types visibles dans la signature, des types de paramètre et le type de retour visible.

Interfaces de binding

Lorsqu'un type calé (shimmed) implémente une interface, le générateur de code émet une méthode qui lui permet de lier tous les membres de cette interface immédiatement.

Par exemple, à partir d'une classe MyClass qui implémente IEnumerable<int>:

public class MyClass : IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() {
        ...
    }
    ...
}

Nous pouvons caler(shim) les implémentations de IEnumerable<int> dans MyClass en appelant la méthode de liaison :

// unit test code
var shimMyClass = new ShimMyClass();
shimMyClass.Bind(new List<int> { 1, 2, 3 });

La structure du type généré ShimMyClass ressemble au code suivant :

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public ShimMyClass Bind(IEnumerable<int> target) {
        ...
    }
}

Modifier le comportement par défaut

Chaque type shim généré contient une instance de l'interface IShimBehavior, via la propriété ShimBase<T>.InstanceBehavior.Ce comportement est utilisée chaque fois qu'un client appelle un membre d'instance qui n'a pas été explicitement calé (shimmed).

Si le comportement n'a pas été définie explicitement, il utilise l'instance retournée par la propriété statique ShimsBehaviors.Current.Par défaut, cette propriété retourne un comportement qui lève une exception NotImplementedException.

Ce comportement peut être modifié à tout moment en affectant à la propriété InstanceBehavior sur toute instance du shim.Par exemple, l'extrait de code suivant remplace le shim par un comportement qui ne fait rien et retourne la valeur par défaut du type de retour, c'est-à-dire valeur par défaut (T) :

// unit test code
var shim = new ShimMyClass();
//return default(T) or do nothing
shim.InstanceBehavior = ShimsBehaviors.DefaultValue;

Le comportement peut également être modifié globalement pour toutes les instances calées (shimmed) pour lesquelles la propriété InstanceBehavior n'a pas été définie explicitement en définissant la propriété statique ShimsBehaviors.Current :

// unit test code
// change default shim for all shim instances
// where the behavior has not been set
ShimsBehaviors.Current = 
    ShimsBehaviors.DefaultValue;

Détecter les accès d'environnement

Il est possible d'attaché un comportement à tous les membres, y compris les méthodes statiques, d'un type particulier en assignant le comportement de ShimsBehaviors.NotImplemented à la propriété statique Behavior du type shim correspondant :

// unit test code
// assigning the not implemented behavior
ShimMyClass.Behavior = ShimsBehaviors.NotImplemented;
// shorthand
ShimMyClass.BehaveAsNotImplemented();

l'accès concurrentiel ;

Les types shim s'appliquent à tous les threads dans l'AppDomain et n'ont pas d'affinité de thread.Il s'agit d'un fait important si vous prévoyez d'utiliser un lanceur de test qui prend en charge l'accès concurrentiel : les tests impliquant des types shim ne peuvent pas s'exécuter simultanément.Cette propriété n'est pas appliquée par l'exécution du Fakes.

Appeler la méthode d'origine de la méthode shim

Supposons que nous voulons réellement écrire le texte vers le système de fichiers après avoir validé le nom du fichier passé à la méthode.Dans ce cas, nous souhaiterions appeler la méthode d'origine au milieu de la méthode shim.

La première approche pour résoudre ce problème consiste à encapsuler un appel à la méthode d'origine à l'aide d'un délégué et ShimsContext.ExecuteWithoutShims() comme dans le code suivant :

// unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
  ShimsContext.ExecuteWithoutShims(() => {

      Console.WriteLine("enter");
      File.WriteAllText(fileName, content);
      Console.WriteLine("leave");
  });
};

Une autre approche consiste à définir le shim à la valeur null, d'appeler la méthode d'origine et de restaurer le shim.

// unit test code
ShimsDelegates.Action<string, string> shim = null;
shim = (fileName, content) => {
  try {
    Console.WriteLine("enter”);
    // remove shim in order to call original method
    ShimFile.WriteAllTextStringString = null;
    File.WriteAllText(fileName, content);
  }
  finally
  {
    // restore shim
    ShimFile.WriteAllTextStringString = shim;
    Console.WriteLine("leave");
  }
};
// initialize the shim
ShimFile.WriteAllTextStringString = shim;

Limitations

Les Shims ne peuvent pas être utilisées sur tous les types de la bibliothèque de classes de base mscorlib et Système .NET.

Ressources externes

Conseils

Test de la livraison continue avec Visual Studio 2012 - Chapitre 2 : Tests unitaires : Tester l'intérieur (page éventuellement en anglais)

Voir aussi

Concepts

Isolation du code sous test avec Microsoft Fakes

Autres ressources

Le blog de Peter Provost : Shims Visual Studio 2012

Vidéo (1:16)  : Le code non testable avec Fakes dans Visual Studio 2012