Partager via


Utiliser des stubs pour isoler des parties de votre application les unes des autres pour des tests unitaires

Les Types Stub sont une technologie importante fournie par l'infrastructure Microsoft Fakes, ce qui facilite l'isolation du composant que vous testez à partir d'autres composants sur lesquels il s'appuie. Un stub agit comme un petit morceau de code qui remplace un autre composant lors du test. Un avantage clé de l'utilisation de stubs est la possibilité d'obtenir des résultats cohérents pour faciliter l'écriture de tests. Même si les autres composants ne sont pas encore entièrement fonctionnels, vous pouvez toujours exécuter des tests à l'aide de stubs.

Pour appliquer efficacement des stubs, il est recommandé de concevoir votre composant de manière à ce qu'il dépend principalement des interfaces plutôt que des classes concrètes provenant d'autres parties de l'application. Cette approche de conception favorise le découplage et réduit la probabilité de changements dans une partie nécessitant des modifications dans une autre. Lorsqu’il s’agit de tester, ce modèle de conception permet de remplacer une implémentation stub pour un composant réel, ce qui facilite l’isolation efficace et les tests précis du composant cible.

Prenons par exemple le diagramme qui illustre les composants impliqués :

Diagram of Real and Stub classes of StockAnalyzer.

Dans ce diagramme, le composant sous test est StockAnalyzer, qui s’appuie généralement sur un autre composant appelé RealStockFeed. Toutefois, RealStockFeed pose un défi pour les tests, car il retourne des résultats différents chaque fois que ses méthodes sont appelées. Cette variabilité rend difficile la vérification cohérente et fiable de StockAnalyzer.

Pour surmonter cet obstacle lors des tests, nous pouvons adopter la pratique de l’injection de dépendances. Cette approche implique l’écriture de votre code de telle sorte qu’il ne mentionne pas explicitement les classes dans un autre composant de votre application. Au lieu de cela, vous définissez une interface que l'autre composant et un stub peuvent implémenter à des fins de test.

Voici un exemple de la façon dont vous pouvez utiliser l’injection de dépendances dans votre code :

public int GetContosoPrice(IStockFeed feed) => feed.GetSharePrice("COOO");

Limitations du stub

Passez en revue les limitations suivantes pour les stubs.

Création d’un stub : guide pas à pas

Commençons cet exercice par un exemple motivant : celui du schéma précédent.

Créer une bibliothèque de classes

Suivez ces étapes pour créer une bibliothèque de classes.

  1. Ouvrez Visual Studio et créez un projet de Bibliothèque de classes.

    Screenshot of Class Library project in Visual Studio.

  2. Configurez les attributs du projet :

    • Définissez le Nom du projet sur StockAnalysis.
    • Définissez le Nom de la solution sur StubsTutorial.
    • Définissez le Frame cible du projet sur .NET 8.0.
  3. Supprimez le fichier par défaut Class1.cs.

  4. Ajoutez un nouveau fichier nommé IStockFeed.cs et copiez-le dans la définition d'interface suivante :

    // IStockFeed.cs
    public interface IStockFeed
    {
        int GetSharePrice(string company);
    }
    
  5. Ajoutez un autre nouveau fichier nommé StockAnalyzer.cs et copiez-le dans la définition de classe suivante :

    // StockAnalyzer.cs
    public class StockAnalyzer
    {
        private IStockFeed stockFeed;
        public StockAnalyzer(IStockFeed feed)
        {
            stockFeed = feed;
        }
        public int GetContosoPrice()
        {
            return stockFeed.GetSharePrice("COOO");
        }
    }
    

Créer un projet de test.

Créez le projet de test pour l'exercice.

  1. Faites un clic droit sur la solution et ajoutez un nouveau projet nommé MSTest Test Project.

  2. Définissez le nom du projet sur TestProject.

  3. Définissez le frame cible du projet sur .NET 8.0.

    Screenshot of Test project in Visual Studio.

Ajouter un assembly Fakes

Ajoutez l'assembly Fakes pour le projet.

  1. Ajouter une référence de projet à StockAnalyzer.

    Screenshot of the command Add Project Reference.

  2. Ajoutez l'assembly Fakes.

    1. Dans l'Explorateur de solutions, recherchez la référence d'assembly :

      • Pour un projet .NET Framework (non SDK) plus ancien, développez le nœud Références de votre projet de test unitaire.

      • Pour un projet de type SDK ciblant le .NET Framework, .NET Core, .NET 5.0 ou une version ultérieure, développez le nœud Dépendances pour rechercher l’assembly à simuler sous Assemblys, Projets ou Packages.

      • Si vous utilisez Visual Basic, sélectionnez Afficher tous les fichiers dans la barre d’outils de l’Explorateur de solutions pour voir le nœud Références.

    2. Sélectionnez l’assembly qui contient les définitions de classes pour lesquelles vous souhaitez créer des shims.

    3. Dans le menu contextuel, sélectionnez Ajouter un assembly Fakes.

      Screenshot of the command Add Fakes Assembly.

Créer un test unitaire

Créez maintenant le test unitaire.

  1. Modifiez le fichier par défaut UnitTest1.cs pour ajouter la définition suivante Test Method.

    [TestClass]
    class UnitTest1
    {
        [TestMethod]
        public void TestContosoPrice()
        {
            // Arrange:
            int priceToReturn = 345;
            string companyCodeUsed = "";
            var componentUnderTest = new StockAnalyzer(new StockAnalysis.Fakes.StubIStockFeed()
            {
                GetSharePriceString = (company) =>
                {
                    // Store the parameter value:
                    companyCodeUsed = company;
                    // Return the value prescribed by this test:
                    return priceToReturn;
                }
            });
    
            // Act:
            int actualResult = componentUnderTest.GetContosoPrice();
    
            // Assert:
            // Verify the correct result in the usual way:
            Assert.AreEqual(priceToReturn, actualResult);
    
            // Verify that the component made the correct call:
            Assert.AreEqual("COOO", companyCodeUsed);
        }
    }
    

    L'ingrédient magique ici est la classe StubIStockFeed. Pour chaque interface de l'assembly référencé, le mécanisme Microsoft Fakes génère une classe stub. Le nom de la classe stub est dérivé du nom de l’interface, précédé de « Fakes.Stub », et auquel les noms de types de paramètre sont ajoutés.

    Les stubs sont également générés pour les accesseurs Get et les méthodes setter de propriétés, les événements et les méthodes génériques. Pour plus d’informations, consultez Utilisation de stubs pour isoler des parties de votre application les unes des autres pour des tests unitaires.

    Screenshot of Solution Explorer showing all files.

  2. Ouvrez l’Explorateur de tests et exécutez le test.

    Screenshot of Test Explorer.

Stubs pour différents types de membres de type

Il existe des stubs pour différents types de membres de type.

Méthodes

Dans l'exemple, les méthodes peuvent être extraites en liant un délégué à une instance de la classe stub. Le nom du type de stub est dérivé des noms de la méthode et des paramètres. Par exemple, considérez l’interface IStockFeed suivante et sa méthode GetSharePrice :

// IStockFeed.cs
interface IStockFeed
{
    int GetSharePrice(string company);
}

Nous attachons un stub à GetSharePrice à l’aide de GetSharePriceString :

// unit test code
var componentUnderTest = new StockAnalyzer(new StockAnalysis.Fakes.StubIStockFeed()
        {
            GetSharePriceString = (company) =>
            {
                // Store the parameter value:
                companyCodeUsed = company;
                // Return the value prescribed by this test:
                return priceToReturn;
            }
        });

Si vous ne fournissez pas de stub pour une méthode, Fakes génère une fonction qui retourne le default value du type de retour. Pour les nombres, la valeur par défaut est 0. Pour les types de classes, la valeur par défaut est null en C# ou Nothing en Visual Basic.

Propriétés

Les accesseurs Get et Set de propriété sont exposés en tant que délégués distincts et peuvent faire l’objet d’un stub séparément. Considérez par exemple la propriété Value de IStockFeedWithProperty :

interface IStockFeedWithProperty
{
    int Value { get; set; }
}

Pour effectuer le stub de Get et Set de Value et simuler une propriété automatique, vous pouvez utiliser le code suivant :

// unit test code
int i = 5;
var stub = new StubIStockFeedWithProperty();
stub.ValueGet = () => i;
stub.ValueSet = (value) => i = value;

Si vous ne fournissez pas les méthodes stub pour la méthode setter ou getter d'une propriété, Microsoft Fakes génère un stub qui stocke les valeurs, afin que la propriété stub se comporte comme une simple variable.

Événements

Les événements sont exposés en tant que champs délégués, ce qui permet à n’importe quel événement faisant l’objet d’un stub d’être déclenché simplement en appelant le champ de stockage d’événement. Prenons l’interface suivante pour un stub :

interface IStockFeedWithEvents
{
    event EventHandler Changed;
}

Pour déclencher l'événement Changed, vous appelez le délégué de stockage :

// unit test code
var withEvents = new StubIStockFeedWithEvents();
// raising Changed
withEvents.ChangedEvent(withEvents, EventArgs.Empty);

Méthodes génériques

Vous pouvez extraire des méthodes génériques en fournissant un délégué pour chaque instanciation souhaitée de la méthode. Par exemple, étant donnée l'interface suivante qui contient une méthode générique :

interface IGenericMethod
{
    T GetValue<T>();
}

Vous pouvez stub l’instanciation GetValue<int> comme suit :

[TestMethod]
public void TestGetValue()
{
    var stub = new StubIGenericMethod();
    stub.GetValueOf1<int>(() => 5);

    IGenericMethod target = stub;
    Assert.AreEqual(5, target.GetValue<int>());
}

Si le code appelle GetValue<T> avec une autre instanciation, le stub exécute le comportement.

Stubs de classes virtuelles

Dans les exemples précédents, les stubs ont été générés à partir d'interfaces. Vous pouvez également générer les stubs à partir d'une classe qui contient des membres virtuels ou abstraits. Par exemple :

// Base class in application under test
public abstract class MyClass
{
    public abstract void DoAbstract(string x);
    public virtual int DoVirtual(int n)
    {
        return n + 42;
    }

    public int DoConcrete()
    {
        return 1;
    }
}

Dans le stub généré à partir de cette classe, vous pouvez définir des méthodes déléguées pour DoAbstract() et DoVirtual(), mais pas pour DoConcrete().

// unit test
var stub = new Fakes.MyClass();
stub.DoAbstractString = (x) => { Assert.IsTrue(x>0); };
stub.DoVirtualInt32 = (n) => 10 ;

Si vous ne fournissez pas de délégué pour une méthode virtuelle, Microsoft Fakes peuvent fournir le comportement par défaut ou appeler la méthode dans la classe de base. Pour que le méthode de base soit appelée, définissez la propriété CallBase :

// unit test code
var stub = new Fakes.MyClass();
stub.CallBase = false;
// No delegate set - default delegate:
Assert.AreEqual(0, stub.DoVirtual(1));

stub.CallBase = true;
// No delegate set - calls the base:
Assert.AreEqual(43,stub.DoVirtual(1));

Changer le comportement par défaut des stubs

Chaque type de stub généré contient une instance de l'interface IStubBehavior via la propriété IStub.InstanceBehavior. Ce comportement est appelé chaque fois qu'un client appelle un membre sans délégué personnalisé attaché. Si le comportement n'est pas défini, il utilise l'instance retournée par la propriété StubsBehaviors.Current. Par défaut, cette propriété retourne un comportement qui lève une exception NotImplementedException.

Le comportement peut être modifié à tout moment en affectant la propriété InstanceBehavior sur toute instance du stub. Par exemple, l’extrait de code suivant change le comportement pour que le stub ne fasse rien ou retourne la valeur par défaut du type de retour default(T) :

// unit test code
var stub = new StockAnalysis.Fakes.StubIStockFeed();
// return default(T) or do nothing
stub.InstanceBehavior = StubsBehaviors.DefaultValue;

Le comportement peut également être modifié globalement pour tous les objets stub où le comportement n'est pas défini avec la propriété StubsBehaviors.Current:

// Change default behavior for all stub instances where the behavior has not been set.
StubBehaviors.Current = BehavedBehaviors.DefaultValue;