Verwenden von Stubs, um für Komponententests Teile der Anwendung voneinander zu trennen

Stubtypen sind eine wichtige Technologie, die vom Microsoft Fakes-Framework bereitgestellt wird. Sie ermöglichen eine einfache Isolierung der zu testenden Komponente von anderen Komponenten, auf die sie angewiesen sind. Ein Stub fungiert als kleiner Codeabschnitt, der während des Tests eine andere Komponente ersetzt. Ein wichtiger Vorteil der Verwendung von Stubs ist die Möglichkeit, konsistente Ergebnisse zu erhalten, um das Schreiben von Tests zu vereinfachen. Auch wenn die anderen Komponenten noch nicht voll funktionsfähig sind, können Sie trotzdem mit Stubs Tests ausführen.

Um Stubs effektiv anwenden zu können, empfiehlt es sich, Ihre Komponente so zu entwerfen, dass sie in erster Linie von Schnittstellen und nicht von konkreten Klassen aus anderen Teilen der Anwendung abhängt. Dieser Entwurfsansatz fördert die Entkopplung und verringert die Wahrscheinlichkeit, dass Änderungen an einem Teil Änderungen an einem anderen Teil erfordern. Wenn es um Tests geht, ermöglicht dieses Entwurfsmuster das Ersetzen einer Stubimplementierung durch eine reale Komponente, was effektive Isolation und genaue Tests der Zielkomponente ermöglicht.

Betrachten wir beispielsweise die Abbildung, die die beteiligten Komponenten zeigt:

Diagram of Real and Stub classes of StockAnalyzer.

In dieser Abbildung ist die getestete Komponente StockAnalyzer, die in der Regel auf einer anderen Komponente namens RealStockFeed basiert. Allerdings stellt RealStockFeed eine Herausforderung für das Testen dar, da bei jedem Aufruf seiner Methoden unterschiedliche Ergebnisse zurückgegeben werden. Diese Variabilität macht es schwierig, konsistente und zuverlässige Tests von StockAnalyzer sicherzustellen.

Um dieses Hindernis während des Tests zu überwinden, können wir die Praxis von Dependency Injection (Abhängigkeitsinjektion) verwenden. Bei diesem Ansatz wird der Code so geschrieben, dass er Klassen in einer anderen Komponente Ihrer Anwendung nicht explizit erwähnt. Stattdessen definieren Sie eine Schnittstelle, die von der anderen Komponente und auch durch einen Stub für Testzwecke implementiert werden kann.

Hier sehen Sie ein Beispiel für die Verwendung von Dependency Injection (Abhängigkeitsinjektion) in Ihrem Code:

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

Einschränkungen für Stubs

Beachten Sie die folgenden Einschränkungen für Stubs.

Erstellen eines Stubs: Schritt-für-Schritt-Anleitung

Beginnen wir diese Übung mit dem anschaulichen Beispiel: in der Abbildung oben.

Erstellen einer Klassenbibliothek

Führen Sie die folgenden Schritte aus, um eine Klassenbibliothek zu erstellen.

  1. Öffnen Sie Visual Studio, und erstellen Sie ein Klassenbibliotheksprojekt.

    Screenshot of Class Library project in Visual Studio.

  2. Konfigurieren Sie die Projektattribute:

    • Legen Sie den Projektnamen auf StockAnalysis fest.
    • Legen Sie den Projektmappennamen auf StubsTutorial fest.
    • Legen Sie das Zielframework des Projekts auf .NET 8.0 fest.
  3. Löschen Sie die Standarddatei Class1.cs.

  4. Fügen Sie eine neue Datei namens IStockFeed.cs hinzu, und kopieren Sie sie in der folgenden Schnittstellendefinition:

    // IStockFeed.cs
    public interface IStockFeed
    {
        int GetSharePrice(string company);
    }
    
  5. Fügen Sie eine weitere Datei namens StockAnalyzer.cs hinzu, und kopieren Sie sie in der folgenden Klassendefinition:

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

Erstellen eines Testprojekts

Erstellen Sie das Testprojekt für die Übung.

  1. Klicken Sie mit der rechten Maustaste auf die Projektmappe, und fügen Sie ein neues Projekt mit dem Namen MSTest Test Project hinzu.

  2. Legen Sie den Projektnamen auf TestProject fest.

  3. Legen Sie das Zielframework des Projekts auf .NET 8.0 fest.

    Screenshot of Test project in Visual Studio.

Hinzufügen von Fakes-Assemblys

Fügen Sie die Fakes-Assembly für das Projekt hinzu.

  1. Fügen Sie StockAnalyzer einen Projektverweis hinzu.

    Screenshot of the command Add Project Reference.

  2. Fügen Sie die Fakes-Assemblys hinzu.

    1. Suchen Sie im Projektmappen-Explorer den Assemblyverweis:

      • Erweitern Sie für ein älteres .NET Framework-Projekt (kein SDK-Format) den Knoten Verweise Ihres Projekts für den Komponententest.

      • Erweitern Sie bei einem Projekt im SDK-Format für .NET Framework, .NET Core oder .NET 5.0 (oder höher) unter Assemblys, Projekte oder Pakete den Knoten Abhängigkeiten, um die gewünschte Assembly zu finden, die Sie als Fakes-Assembly verwenden möchten.

      • Wenn Sie in Visual Basic arbeiten, müssen Sie auf der Symbolleiste im Projektmappen-Explorer auf Alle Dateien anzeigen klicken, um den Knoten Verweise anzuzeigen.

    2. Wählen Sie die Assembly aus, in der die Klassendefinitionen enthalten sind, für die Sie Stubs erstellen möchten.

    3. Wählen Sie im Kontextmenü Fakes-Assembly hinzufügen aus.

      Screenshot of the command Add Fakes Assembly.

Einen Komponententest erstellen

Erstellen Sie nun den Komponententest.

  1. Ändern Sie die Standarddatei UnitTest1.cs-, um die folgende Test Method-Definition hinzuzufügen.

    [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);
        }
    }
    

    Das Besondere hierbei ist die StubIStockFeed-Klasse. Der Microsoft Fakes-Mechanismus generiert für jede Schnittstelle in der Assembly, auf die verwiesen wird, eine Stubklasse. Der Name der Stubklasse wird vom Namen der Schnittstelle abgeleitet. Dabei ist Fakes.Stub das Präfix, und die Parametertypnamen werden angefügt.

    Stubs werden auch für die Getter und Setter von Eigenschaften, für Ereignisse sowie für generische Methoden generiert. Weitere Informationen finden Sie unter Use stubs to isolate parts of your application from each other for unit testing (Verwenden von Stubs, um Teile der Anwendung für Komponententests voneinander zu isolieren).

    Screenshot of Solution Explorer showing all files.

  2. Öffnen Sie den Test-Explorer, und führen Sie den Test aus.

    Screenshot of Test Explorer.

Stubs für verschiedene Arten von Typmembern

Es gibt Stubs für verschiedene Arten von Typmembern.

Methoden

Im gezeigten Beispiel kann ein Stub für Methoden ausgeführt werden, indem ein Delegat an eine Instanz der Stubklasse angefügt wird. Der Name des Stub-Typs wird von den Namen der Methode und der Parameter abgeleitet. Sehen Sie sich beispielsweise die folgende IStockFeed-Schnittstelle und ihre GetSharePrice-Methode an:

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

Wir fügen einen Stub mit GetSharePriceString an GetSharePrice an:

// 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;
            }
        });

Wenn Sie keinen Stub für eine Methode bereitstellen, generiert Fakes eine Funktion, die den default value des Rückgabetyps zurückgibt. Für Zahlen lautet der Standardwert 0 (null). Für Klassentypen ist die Standardeinstellung null in C# oder Nothing in Visual Basic.

Eigenschaften

Getter oder Setter für Eigenschaften werden als separate Delegaten verfügbar gemacht. Stubbing kann für diese Delegaten einzeln ausgeführt werden. Ziehen Sie zum Beispiel die Value-Eigenschaft von IStockFeedWithProperty in Erwägung:

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

Um den Getter und Setter von Value zu stubben und eine automatische Eigenschaft zu simulieren, können Sie den folgenden Code verwenden:

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

Wenn Sie keine Stubmethoden für den Setter oder den Getter einer Eigenschaft bereitstellen, generiert Fakes einen Stub, der Werte speichert, sodass sich die Stubeigenschaft wie eine einfache Variable verhält.

Ereignisse

Ereignisse werden als Delegatfelder verfügbar gemacht, sodass jedes gestubbte Ereignis einfach durch Aufrufen des Unterstützungsfelds des Ereignisses ausgelöst werden kann. Betrachten wir die folgende Schnittstelle, für die ein Stub ausgeführt werden soll:

interface IStockFeedWithEvents
{
    event EventHandler Changed;
}

Um das Changed-Ereignis auszulösen, rufen Sie den dahinter liegenden Delegat auf:

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

Generische Methoden

Sie können einen Stub mit generischen Methoden verwenden, indem ein Delegat für jede gewünschte Instanziierung der Methode bereitgestellt wird. Betrachten Sie beispielsweise die folgende Schnittstelle, die eine generische Methode enthält:

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

Sie können die GetValue<int>-Instanziierung wie folgt stubben:

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

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

Ruft der Code GetValue<T> mit einer anderen Instanziierung auf, führt der Stub das Verhalten aus.

Stubs von virtuellen Klassen

In den vorherigen Beispielen wurden die Stubs aus Schnittstellen generiert. Sie können Stubs jedoch auch aus einer Klasse generieren, die über virtuelle oder abstrakte Member verfügt. Beispiel:

// 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;
    }
}

In dem Stub, der aus dieser Klasse generiert wird, können Sie Delegatmethoden für DoAbstract() und DoVirtual(), aber nicht für DoConcrete() festlegen.

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

Wenn Sie keinen Delegaten für eine virtuelle Methode bereitstellen, kann Fakes entweder das Standardverhalten bereitstellen oder die Methode in der Basisklasse aufrufen. Legen Sie die CallBase-Eigenschaft zum Aufrufen der Basismethode wie folgt fest:

// 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));

Ändern des Standardverhaltens von Stubs

Jeder generierte Stubtyp enthält eine Instanz der IStubBehavior-Schnittstelle durch die IStub.InstanceBehavior-Eigenschaft. Dieses Verhalten wird aufgerufen, wenn ein Client einen Member ohne angefügten benutzerdefinierten Delegaten aufruft. Wenn das Verhalten nicht festgelegt wurde, wird die Instanz verwendet, die durch die StubsBehaviors.Current-Eigenschaft zurückgegeben wird. Standardmäßig gibt diese Eigenschaft ein Verhalten zurück, das eine NotImplementedException-Ausnahme auslöst.

Sie können das Verhalten jederzeit durch Festlegen der InstanceBehavior-Eigenschaft auf jede beliebige Stubinstanz ändern. Beispielsweise ändert der folgende Codeschnipsel das Verhalten so, dass der Stub keine Aktion ausführt oder den Standardwert des Rückgabetyps default(T) zurückgibt:

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

Das Verhalten kann auch global für alle Stubobjekte, für die das Verhalten nicht mit der StubsBehaviors.Current-Eigenschaft festgelegt wurde:

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