Freigeben über


Bewährte Methoden für Komponententests für .NET

Es gibt zahlreiche Vorteile beim Schreiben von Unit-Tests. Sie helfen bei Regressionen, stellen Dokumentationen bereit und erleichtern ein gutes Design. Aber wenn Unit-Tests schwer zu lesen und instabil sind, können sie Chaos in Ihrem Code verursachen. In diesem Artikel werden einige bewährte Methoden zum Entwerfen von Komponententests beschrieben, um Ihre .NET Core- und .NET Standard-Projekte zu unterstützen. Sie lernen Techniken, um Ihre Tests robust und verständlich zu machen.

Von John Reese mit besonderem Dank an Roy Osherove

Vorteile des Komponententests

In den folgenden Abschnitten werden mehrere Gründe zum Schreiben von Komponententests für Ihre .NET Core- und .NET Standard-Projekte beschrieben.

Weniger Zeit beim Ausführen von Funktionstests

Funktionale Tests sind teuer. Sie umfassen in der Regel das Öffnen der Anwendung und das Ausführen einer Reihe von Schritten, die Sie (oder eine andere Person) ausführen müssen, um das erwartete Verhalten zu überprüfen. Diese Schritte sind dem Tester möglicherweise nicht immer bekannt. Sie müssen sich an jemanden wenden, der in diesem Bereich mehr Kenntnis hat, um den Test durchzuführen. Das Testen selbst kann bei trivialen Änderungen Sekunden und bei größeren Änderungen Minuten dauern. Schließlich muss dieser Prozess für jede Änderung, die Sie im System vornehmen, wiederholt werden. Modultests hingegen dauern Millisekunden, können auf Knopfdruck ausgeführt werden und benötigen nicht unbedingt Kenntnisse des Systems. Der Testläufer bestimmt, ob der Test bestanden oder fehlschlägt, nicht die Person.

Schutz vor Regression

Regressionsfehler sind Fehler, die beim Ändern der Anwendung eingeführt werden. Es ist üblich, dass Tester nicht nur ihr neues Feature testen, sondern auch vorab vorhandene Features testen, um zu überprüfen, ob vorhandene Features weiterhin wie erwartet funktionieren. Mit Komponententests können Sie ihre gesamte Suite von Tests nach jedem Build oder sogar nach dem Ändern einer Codezeile erneut ausführen. Dieser Ansatz trägt dazu bei, das Vertrauen zu erhöhen, dass ihr neuer Code vorhandene Funktionen nicht unterbricht.

Ausführbare Dokumentation

Es ist möglicherweise nicht immer offensichtlich, was eine bestimmte Methode tut oder wie sie sich bei einer bestimmten Eingabe verhält. Sie fragen sich vielleicht: Wie verhält sich diese Methode, wenn ich sie an eine leere Zeichenfolge oder null passe? Wenn Sie über eine Reihe von gut benannten Komponententests verfügen, sollte jeder Test die erwartete Ausgabe für eine bestimmte Eingabe deutlich erklären. Darüber hinaus sollte der Test in der Lage sein, zu überprüfen, ob er tatsächlich funktioniert.

Weniger gekoppelter Code

Wenn Code eng gekoppelt ist, kann es schwierig sein, den Komponententest zu testen. Ohne Komponententests für den Code, den Sie schreiben, kann die Kopplung weniger offensichtlich sein. Das Schreiben von Tests für Ihren Code entkoppelt Ihren Code natürlich, da es sonst schwieriger zu testen ist.

Eigenschaften guter Komponententests

Es gibt mehrere wichtige Merkmale, die einen guten Komponententest definieren:

  • Schnell: Es ist nicht ungewöhnlich, dass reife Projekte Tausende von Komponententests haben. Komponententests sollten wenig Zeit in Anspruch nehmen. Millisekunden.
  • Isoliert: Komponententests sind eigenständig, können isoliert ausgeführt werden und haben keine Abhängigkeiten von externen Faktoren, z. B. einem Dateisystem oder einer Datenbank.
  • Wiederholbar: Das Ausführen eines Komponententests sollte mit seinen Ergebnissen konsistent sein. Der Test gibt immer dasselbe Ergebnis zurück, wenn Sie zwischen den Ausführungen nichts ändern.
  • Selbstüberprüfung: Der Test sollte automatisch erkennen, ob er ohne menschliche Interaktion bestanden oder fehlgeschlagen ist.
  • Zeitnah: Die Erstellung eines Einheitstests sollte nicht unverhältnismäßig viel Zeit in Anspruch nehmen, verglichen mit dem zu testenden Code. Wenn Sie feststellen, dass das Testen des Codes im Vergleich zum Schreiben des Codes eine große Zeit in Anspruch nimmt, sollten Sie ein testbareres Design in Betracht ziehen.

Codeabdeckung und Codequalität

Ein hoher Codeabdeckungsprozentsatz ist häufig mit einer höheren Codequalität verknüpft. Die Messung selbst kann jedoch nicht die Codequalität bestimmen. Das Festlegen eines übermäßig ehrgeizigen Codeabdeckungsprozentziels kann kontraproduktiv sein. Nehmen wir an, Sie haben für ein komplexes Projekt mit Tausenden von bedingten Branches das Ziel einer Code-Abdeckung von 95 % festgelegt. Aktuell hat das Projekt eine Code-Abdeckung von 90 %. Der Zeitaufwand, der erforderlich ist, um alle Edge-Fälle in den verbleibenden 5 % zu berücksichtigen, kann ein gewaltiges Unterfangen sein, so dass der Wertbeitrag schnell abnimmt.

Ein Prozentsatz der hohen Codeabdeckung ist kein Indikator für den Erfolg, und er impliziert keine hohe Codequalität. Sie stellt lediglich die Menge des Codes dar, der von Komponententests abgedeckt wird. Weitere Informationen finden Sie unter Verwenden von Code Coverage für Komponententests.

Terminologie für Komponententests

Im Kontext von Komponententests werden häufig mehrere Begriffe verwendet: Fake, Mock und Stub. Leider können diese Begriffe falsch angewendet werden, daher ist es wichtig, die richtige Verwendung zu verstehen.

  • Fake: Ein Fake ist ein allgemeiner Begriff, der entweder für einen Stub oder ein Scheinobjekt verwendet werden kann. Ob es sich bei dem Objekt um einen Stub oder ein Mock handelt, hängt vom Kontext ab, in dem das Objekt verwendet wird. Mit anderen Worten, ein Fake kann ein Stub oder ein Mock sein.

  • Mock: Ein Pseudoobjekt ist ein gefälschtes Objekt im System, das entscheidet, ob ein Komponententest bestanden oder nicht erfolgreich ist. Ein Mock beginnt als Fake und bleibt ein Fake, bis er in einen Assert-Vorgang eintritt.

  • Stub: Ein Stub ist ein steuerbarer Ersatz für eine bestehende Abhängigkeit (oder einen Mitspieler) im System. Mithilfe eines Stubs können Sie Ihren Code testen, ohne sich direkt mit der Abhängigkeit zu befassen. Standardmäßig beginnt ein Stub als Fake.

Beachten Sie den folgenden Code:

var mockOrder = new MockOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Dieser Code zeigt einen Stub, der als Mock bezeichnet wird. Aber in diesem Szenario ist der Stub wirklich ein Stub. Der Zweck des Codes besteht darin, die Reihenfolge als Mittel zu übergeben, um das Purchase Objekt (das System unter Test) instanziieren zu können. Der Klassenname MockOrder ist irreführend, denn es handelt sich um einen Stub und nicht um einen Mock.

Der folgende Code zeigt ein genaueres Design:

var stubOrder = new FakeOrder();
var purchase = new Purchase(stubOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Wenn die Klasse umbenannt FakeOrderwird, ist die Klasse allgemeiner. Die Klasse kann als Mock oder als Stub verwendet werden, je nach den Anforderungen des Testfalls. Im ersten Beispiel wird die FakeOrder Klasse als Stub verwendet, und sie wird während des Assert Vorgangs nicht verwendet. Der Code übergibt die FakeOrder Klasse an die Purchase Klasse, um die Anforderungen des Konstruktors zu erfüllen.

Um die Klasse als Mock zu verwenden, können Sie den Code aktualisieren:

var mockOrder = new FakeOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(mockOrder.Validated);

In diesem Design überprüft der Code eine Eigenschaft der Fälschung (indem er dagegen eine Assertion durchführt), und daher ist die mockOrder Klasse ein Mock.

Von Bedeutung

Es ist wichtig, die Terminologie richtig zu implementieren. Wenn Sie Ihre Stubs als „Mocks“ bezeichnen, können andere Entwickler Ihre Absicht falsch verstehen.

Das Wichtigste, was Sie sich über Mocks und Stubs merken sollten, ist, dass Mocks genau wie Stubs sind, mit Ausnahme des Assert-Prozesses. Sie führen Assert-Operationen gegen ein Mock-Objekt aus, aber nicht gegen einen Stub.

Bewährte Methoden

Es gibt mehrere wichtige bewährte Methoden zum Schreiben von Komponententests. Die folgenden Abschnitte enthalten Beispiele, die zeigen, wie sie die bewährten Methoden auf Ihren Code anwenden.

Vermeiden von Infrastrukturabhängigkeiten

Versuchen Sie nicht, Abhängigkeiten von der Infrastruktur beim Schreiben von Komponententests einzuführen. Die Abhängigkeiten machen die Tests langsam und spröde und sollten für Integrationstests reserviert sein. Sie können diese Abhängigkeiten in Ihrer Anwendung vermeiden, indem Sie das Explizite Abhängigkeitsprinzip und die .NET-Abhängigkeitsinjektion verwenden. Sie können Ihre Komponententests auch in einem separaten Projekt von Ihren Integrationstests trennen. Dieser Ansatz stellt sicher, dass Ihr Komponententestprojekt keine Verweise auf oder Abhängigkeiten von Infrastrukturpaketen enthält.

Befolgen Sie die Testbenennungsstandards

Der Name des Tests sollte aus drei Teilen bestehen:

  • Name der getesteten Methode
  • Szenario, unter dem die Methode getestet wird
  • Erwartetes Verhalten beim Aufrufen des Szenarios

Benennungsstandards sind wichtig, da sie dazu beitragen, den Testzweck und die Anwendung auszudrücken. Tests sind mehr als nur sicherzustellen, dass Ihr Code funktioniert. Sie enthalten auch Dokumentationen. Nur wenn Sie sich die Suite der Unittests ansehen, sollten Sie das Verhalten Ihres Codes ableiten, ohne den Code selbst betrachten zu müssen. Darüber hinaus können Sie, wenn Tests fehlschlagen, genau sehen, welche Szenarien Ihre Erwartungen nicht erfüllen.

Originalcode

[Fact]
public void Test_Single()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

Bewährte Methode anwenden

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

Anordnen ihrer Tests

Das Muster "Arrange, Act, Assert" ist ein gängiger Ansatz zum Schreiben von Komponententests. Wie der Name schon sagt, besteht das Muster aus drei Hauptaufgaben:

  • Ordnen Sie Ihre Objekte an, erstellen und konfigurieren Sie sie nach Bedarf.
  • Reagieren auf ein Objekt
  • Bestätigen, dass etwas wie erwartet ist

Wenn Sie dem Muster folgen, können Sie klar unterscheiden, was getestet wird, von den Aufgaben 'Anordnen' und 'Assert'. Das Muster hilft auch dabei, die Gelegenheit zu verringern, dass Assertionen mit Code in der Act-Aufgabe vermischt werden.

Die Lesbarkeit ist einer der wichtigsten Aspekte beim Schreiben eines Komponententests. Durch das Trennen jeder Musteraktion innerhalb des Tests werden die Abhängigkeiten deutlich hervorgehoben, die erforderlich sind, um Ihren Code aufzurufen, wie Ihr Code aufgerufen wird und was Sie zu überprüfen versuchen. Obwohl es möglich sein kann, einige Schritte zu kombinieren und die Größe Ihres Tests zu verringern, besteht das Gesamtziel darin, den Test so lesbar wie möglich zu machen.

Originalcode

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Assert
    Assert.Equal(0, stringCalculator.Add(""));
}

Bewährte Methode anwenden

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Act
    var actual = stringCalculator.Add("");

    // Assert
    Assert.Equal(0, actual);
}

Schreiben von minimal erfolgreichen Tests

Die Eingabe für einen Komponententest sollte die einfachsten Informationen sein, die zum Überprüfen des aktuell getesteten Verhaltens erforderlich sind. Der minimalistische Ansatz hilft Tests, widerstandsfähiger für zukünftige Änderungen in der Codebasis zu werden und sich auf die Überprüfung des Verhaltens über die Implementierung zu konzentrieren.

Tests, die mehr Informationen enthalten als erforderlich, um den aktuellen Test zu bestehen, haben eine höhere Wahrscheinlichkeit, Fehler in den Test einzuführen und die Absicht des Tests weniger deutlich zu machen. Beim Schreiben von Tests möchten Sie sich auf das Verhalten konzentrieren. Wenn Sie zusätzliche Eigenschaften für Modelle festlegen oder Nicht-Null-Werte verwenden, obwohl sie nicht erforderlich sind, lenken Sie von dem ab, was Sie bestätigen möchten.

Originalcode

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("42");

    Assert.Equal(42, actual);
}

Bewährte Methode anwenden

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

Vermeiden „magischer“ Zeichenfolgen

Magic Strings sind Zeichenfolgen, die direkt in Ihre Komponententests hartcodiert werden, ohne jeglichen zusätzlichen Kommentar oder Kontext im Code. Diese Werte machen ihren Code weniger lesbar und schwieriger zu verwalten. Magische Zeichenfolgen können für den Leser Ihrer Tests Verwirrung verursachen. Wenn eine Zeichenfolge aus dem Normalen heraussieht, fragen sie sich möglicherweise, warum ein bestimmter Wert für einen Parameter oder Rückgabewert ausgewählt wurde. Diese Art von Zeichenfolgenwert kann dazu führen, dass sie sich genauer mit den Implementierungsdetails befassen, anstatt sich auf den Test zu konzentrieren.

Tipp

Machen Sie es zu Ihrem Ziel, so viel Absicht wie möglich in Ihrem Unit-Test-Code auszudrücken. Anstatt magische Zeichenfolgen zu verwenden, weisen Sie Konstanten alle hartcodierten Werte zu.

Originalcode

[Fact]
public void Add_BigNumber_ThrowsException()
{
    var stringCalculator = new StringCalculator();

    Action actual = () => stringCalculator.Add("1001");

    Assert.Throws<OverflowException>(actual);
}

Bewährte Methode anwenden

[Fact]
void Add_MaximumSumResult_ThrowsOverflowException()
{
    var stringCalculator = new StringCalculator();
    const string MAXIMUM_RESULT = "1001";

    Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);

    Assert.Throws<OverflowException>(actual);
}

Vermeiden von Codierungslogik in Komponententests

Vermeiden Sie beim Schreiben von Komponententests manuelle Zeichenfolgenverkettung, logische Bedingungen wie if, , while, forund switchandere Bedingungen. Wenn Sie Logik in Ihre Testsuite einschließen, erhöht sich die Wahrscheinlichkeit, Fehler einzuführen, erheblich. Der letzte Ort, an dem Sie einen Fehler finden möchten, befindet sich in Ihrer Testsuite. Sie sollten ein hohes Maß an Vertrauen haben, dass Ihre Tests funktionieren, andernfalls können Sie sie nicht vertrauen. Tests, denen Sie nicht vertrauen, geben keinen Wert an. Wenn ein Test fehlschlägt, möchten Sie einen Eindruck haben, dass etwas mit Ihrem Code falsch ist und dass er nicht ignoriert werden kann.

Tipp

Wenn das Hinzufügen von Logik in Ihrem Test unvermeidbar erscheint, sollten Sie den Test in zwei oder mehr verschiedene Tests aufteilen, um die Logikanforderungen zu begrenzen.

Originalcode

[Fact]
public void Add_MultipleNumbers_ReturnsCorrectResults()
{
    var stringCalculator = new StringCalculator();
    var expected = 0;
    var testCases = new[]
    {
        "0,0,0",
        "0,1,2",
        "1,2,3"
    };

    foreach (var test in testCases)
    {
        Assert.Equal(expected, stringCalculator.Add(test));
        expected += 3;
    }
}

Bewährte Methode anwenden

[Theory]
[InlineData("0,0,0", 0)]
[InlineData("0,1,2", 3)]
[InlineData("1,2,3", 6)]
public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add(input);

    Assert.Equal(expected, actual);
}

Verwenden von Hilfsmethoden anstelle von Setup und Teardown

Wenn Sie ein ähnliches Objekt oder einen ähnlichen Zustand für Ihre Tests benötigen, verwenden Sie eine Hilfsmethode anstelle Setup von Teardown Attributen, falls vorhanden. Hilfsmethoden werden aus mehreren Gründen gegenüber diesen Attributen bevorzugt:

  • Weniger Verwirrung beim Lesen der Tests, da der gesamte Code innerhalb der einzelnen Tests sichtbar ist
  • Weniger Wahrscheinlichkeit, für den gegebenen Test zu viel oder zu wenig einzurichten
  • Weniger Wahrscheinlichkeit, dass der Status zwischen Tests geteilt wird, wodurch unerwünschte Abhängigkeiten zwischen ihnen entstehen

In Komponententestframeworks wird das Setup Attribut vor jedem und jedem Komponententest in Ihrer Testsuite aufgerufen. Einige Programmierer sehen dieses Verhalten als nützlich, aber es führt oft zu aufgeblähten und schwer zu lesenden Tests. Jeder Test hat in der Regel unterschiedliche Anforderungen für Setup und Ausführung. Leider zwingt das Setup Attribut Sie, die genauen Anforderungen für jeden Test zu verwenden.

Hinweis

Die Attribute SetUp und TearDown werden in xUnit-Version 2.x und später entfernt.

Originalcode

Bewährte Methode anwenden

private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
    stringCalculator = new StringCalculator();
}
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var stringCalculator = CreateDefaultStringCalculator();

    var actual = stringCalculator.Add("0,1");

    Assert.Equal(1, actual);
}
// More tests...
// More tests...
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var result = stringCalculator.Add("0,1");

    Assert.Equal(1, result);
}
private StringCalculator CreateDefaultStringCalculator()
{
    return new StringCalculator();
}

Vermeiden Sie mehrere Act-Aufgaben

Wenn Sie Ihre Tests schreiben, versuchen Sie, nur eine Act-Aufgabe pro Test einzuschließen. Einige gängige Ansätze für die Implementierung einer einzelnen Act-Aufgabe umfassen das Erstellen eines separaten Tests für jeden Act oder die Verwendung parametrisierter Tests. Es gibt mehrere Vorteile für die Verwendung einer einzelnen Act-Aufgabe für jeden Test:

  • Sie können leicht erkennen, welche Act-Aufgabe fehlschlägt, wenn der Test fehlschlägt.
  • Sie können sicherstellen, dass sich der Test nur auf einen einzelnen Fall konzentriert.
  • Sie erhalten ein klares Bild darüber, warum Ihre Tests fehlschlagen.

Mehrere Act-Aufgaben müssen einzeln bestätigt werden, und Sie können nicht garantieren, dass alle Assert-Aufgaben ausgeführt werden. In den meisten Komponententestframeworks werden alle nachfolgenden Tests automatisch als Fehlschlagen betrachtet, nachdem eine Assert-Aufgabe in einem Komponententest fehlschlägt. Der Prozess kann verwirrend sein, da einige funktionsfähige Funktionen möglicherweise als fehlgeschlagen interpretiert werden.

Originalcode

[Fact]
public void Add_EmptyEntries_ShouldBeTreatedAsZero()
{
    // Act
    var actual1 = stringCalculator.Add("");
    var actual2 = stringCalculator.Add(",");

    // Assert
    Assert.Equal(0, actual1);
    Assert.Equal(0, actual2);
}

Bewährte Methode anwenden

[Theory]
[InlineData("", 0)]
[InlineData(",", 0)]
public void Add_EmptyEntries_ShouldBeTreatedAsZero(string input, int expected)
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Act
    var actual = stringCalculator.Add(input);

    // Assert
    Assert.Equal(expected, actual);
}

Überprüfen privater Methoden mit öffentlichen Methoden

In den meisten Fällen müssen Sie keine private Methode in Ihrem Code testen. Private Methoden sind ein Implementierungsdetails und existieren nie isoliert. An einem bestimmten Punkt im Entwicklungsprozess führen Sie eine öffentlich zugängliche Methode ein, um die private Methode als Teil der Implementierung aufzurufen. Wenn Sie Ihre Einheitstests schreiben, interessiert Sie nur das Endergebnis der öffentlichen Methode, die die private Methode aufruft.

Betrachten Sie das folgende Codeszenario:

public string ParseLogLine(string input)
{
    var sanitizedInput = TrimInput(input);
    return sanitizedInput;
}

private string TrimInput(string input)
{
    return input.Trim();
}

Im Hinblick auf Tests könnte Ihre erste Reaktion darin sein, einen Test für die TrimInput Methode zu schreiben, um sicherzustellen, dass sie erwartungsgemäß funktioniert. Es ist jedoch möglich, dass die ParseLogLine Methode das sanitizedInput Objekt auf eine Weise bearbeitet, die Sie nicht erwarten. Das unbekannte Verhalten kann Ihren Test für die TrimInput Methode nutzlos rendern.

Ein besserer Test in diesem Szenario besteht darin, die öffentlich zugängliche ParseLogLine Methode zu überprüfen:

public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
{
    var parser = new Parser();

    var result = parser.ParseLogLine(" a ");

    Assert.Equals("a", result);
}

Wenn Sie auf eine private Methode stoßen, suchen Sie die öffentliche Methode, die die private Methode aufruft, und schreiben Sie Ihre Tests anhand der öffentlichen Methode. Da eine private Methode ein erwartetes Ergebnis zurückgibt, bedeutet dies nicht, dass das System, das schließlich die private Methode aufruft, das Ergebnis korrekt verwendet.

Statische Stub-Referenzen mit Seams behandeln

Ein Prinzip eines Komponententests besteht darin, dass es die volle Kontrolle über das zu testende System haben muss. Dieses Prinzip kann jedoch problematisch sein, wenn Produktionscode Aufrufe statischer Verweise enthält (z. B DateTime.Now. ).

Überprüfen Sie das folgende Codeszenario:

public int GetDiscountedPrice(int price)
{
    if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}

Können Sie einen Komponententest für diesen Code schreiben? Möglicherweise versuchen Sie, eine Assert-Aufgabe für folgendes priceauszuführen:

public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(2, actual)
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(1, actual);
}

Leider erkennen Sie schnell, dass es einige Probleme mit Ihrem Test gibt:

  • Wenn die Testsuite am Dienstag ausgeführt wird, besteht der zweite Test, aber der erste Test schlägt fehl.
  • Wenn die Testsuite an einem anderen Tag ausgeführt wird, besteht der erste Test, aber der zweite Test schlägt fehl.

Um diese Probleme zu lösen, müssen Sie einen Naht in Ihren Produktionscode einführen. Ein Ansatz besteht darin, den Code umzuschließen, den Sie in einer Schnittstelle steuern müssen, und der Produktionscode hängt von dieser Schnittstelle ab:

public interface IDateTimeProvider
{
    DayOfWeek DayOfWeek();
}

public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
{
    if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}

Außerdem müssen Sie eine neue Version Ihrer Testsuite schreiben:

public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Monday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(2, actual);
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(1, actual);
}

Jetzt hat die Suite die volle Kontrolle über den DateTime.Now-Wert und kann jeden Wert beim Aufruf der Methode stubben.