Bewährte Methoden für Komponententests mit .NET Core und .NET Standard

Komponententests bringen zahlreiche Vorteile mit sich: Sie helfen bei der Regression, stellen Dokumentation bereit und unterstützen Sie beim Entwerfen guten Codes. Allerdings können Komponententest, die schwierig zu lesen und fehleranfällig sind, zu einer unübersichtlichen Codebasis führen. In diesem Artikel werden einige bewährte Methoden in Bezug auf den Entwurf von Komponententests für Ihre .NET Core- und .NET Standard-Projekte beschrieben.

In diesem Leitfaden lernen Sie die bewährten Methoden beim Schreiben von Komponententests kennen, mit denen Sie Ihre Tests resilient und leicht verständlich gestalten können.

Von John Reese mit besonderem Dank an Roy Osherove

Weshalb sollte ich Komponententests durchführen?

Es gibt mehrere Gründe, Komponententests zu verwenden.

Weniger Zeitaufwand für das Ausführen von Funktionstests

Funktionstests sind kostspielig. Für Funktionstests muss normalerweise die Anwendung geöffnet werden, und Sie (oder eine andere Person) müssen mehrere Schritte ausführen, 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 Kenntnisse hat, um den Test durchzuführen. Der Testvorgang selbst kann nur ein paar Sekunden für banale Änderungen oder Minuten für größere Änderungen in Anspruch nehmen. Abschließend muss der ganze Vorgang für jede Änderung, die Sie am System vornehmen, wiederholt werden.

Im Vergleich dazu benötigen Komponententests nur Millisekunden. Außerdem können sie durch einen Klick auf eine Schaltfläche ausgeführt werden und erfordern keine tiefgreifenden Kenntnisse zum System. Ob der Test erfolgreich ist oder fehlschlägt, hängt vom Testlauf und nicht von der einzelnen Person ab, die den Test ausführt.

Schutz vor Regression

Regressionsfehler sind Fehler, die auftreten, wenn eine Änderung an der Anwendung vorgenommen wird. Tester testen meist nicht nur ihr neues Feature sondern auch die Features, die zuvor schon vorhanden waren. So können sie überprüfen, ob die zuvor implementierten Features noch immer wie erwartet funktionieren.

Durch Komponententests ist es möglich, die gesamte Testsammlung nach jedem Build oder sogar nach jeder Änderung an einer Codezeile erneut durchzuführen. So können Sie sicherstellen, dass Ihr neuer Code die vorhandene Funktionalität nicht beeinträchtigt.

Dokumentation zur Ausführbarkeit

Es ist vielleicht nicht immer offensichtlich, was genau 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 ihr eine leere Zeichenfolge übergebe? Oder was geschieht, wenn ich NULL übergebe?

Wenn Sie über eine Sammlung von sinnvoll benannten Komponententests verfügen, sollte jeder Test die erwartete Ausgabe für eine gegebene Eingabe eindeutig erklären können. Zusätzlich sollte er überprüfen, ob sie tatsächlich funktioniert.

Weniger gekoppelter Code

Wenn Code eng gekoppelt ist, kann es schwierig sein, Komponententests dafür auszuführen. Ohne das Erstellen von Komponententests für den von Ihnen geschriebenen Code könnte die Kopplung weniger offensichtlich sein.

Wenn Sie Tests für Ihren Code schreiben, wird dieser automatisch entkoppelt, da es ansonsten schwieriger wäre, diesen zu testen.

Merkmale eines guten Komponententests

  • Schnell: Es ist nicht ungewöhnlich, dass ausgereifte Projekte Tausende von Komponententests enthalten. Die Ausführung von Komponententests sollte wenig Zeit in Anspruch nehmen. Sie sollten innerhalb von Millisekunden abgeschlossen sein.
  • Isoliert: Komponententest sind eigenständig, sie können isoliert ausgeführt werden und verfügen über keine Abhängigkeiten auf externen Faktoren, wie etwa einem Dateisystem oder einer Datenbank.
  • Wiederholbar: Das Ausführen eines Komponententests sollte immer zu den gleichen Ergebnissen führen. Wenn Sie zwischen den Ausführungen nichts ändern, sollte sich das Ergebnis auch nicht ändern.
  • Selbstprüfung: Der Test sollte in der Lage sein, automatisch und ohne menschliches Eingreifen zu erkennen, ob er erfolgreich war oder nicht.
  • Angemessener Zeitrahmen: Ein Komponententest darf verglichen mit dem zu testenden Code nicht unverhältnismäßig lang für das Schreiben benötigen. Wenn Sie bemerken, dass der Codetest im Vergleich zum Schreiben des Codes viel Zeit in Anspruch nimmt, erwägen Sie einen Entwurf, der besser getestet werden kann.

Codeabdeckung

Ein hoher Code-Coverage-Prozentsatz steht oft im Zusammenhang mit einer höheren Codequalität. Durch die Messung an sich kann die Codequalität jedoch nicht bestimmt werden. Das Festlegen eines übermäßig ambitionierten Code-Coverage-Prozentsatzes kann kontraproduktiv sein. Stellen Sie sich ein komplexes Projekt mit Tausenden von bedingten Branches vor, und stellen Sie sich vor, dass Sie ein Code-Coverage-Ziel von 95 % festlegen. Derzeit weist das Projekt 90 % Code Coverage auf. Für die restlichen 5 % könnte für alle Grenzfälle sehr viel Zeit einkalkuliert werden müssen, und der Wertbeitrag verringert sich schnell.

Ein hoher Code-Coverage-Prozentsatz ist weder ein Erfolgsindikator noch ein Hinweis auf eine hohe Codequalität. Er stellt lediglich die Menge an Code dar, die durch Komponententests abgedeckt wird. Weitere Informationen finden Sie unter Verwenden von Code Coverage für Komponententests.

Begriffsklärung

Der Begriff Mock (Deutsch: „Pseudo“) wird leider im Testkontext häufig falsch verwendet. Im Folgenden werden die häufigsten Arten von unechten Objekten, sogenannten Fakes, beim Schreiben von Komponententests definiert:

Fake: Fake ist ein generischer Begriff, der sowohl für Stubs als auch für Pseudoobjekte („Mocks“) verwendet wird. Ob es sich um einen Stub oder um einen Mock handelt, hängt vom Kontext ab. Das bedeutet, dass ein Fake entweder ein Stub oder ein Mock sein kann.

Mock: Ein Pseudoobjekt ist ein unechtes Objekt im System, das entscheidet, ob ein Komponententest erfolgreich war oder nicht. Ein Mock ist zunächst ein Fake, bis eine Assert-Anweisung ausgeführt wird.

Stub: Ein Stub ist ein anpassbarer Platzhalter für eine vorhandene Abhängigkeit (oder einen Projektmitarbeiter) im System. Wenn Sie einen Stub verwenden, können Sie Ihren Code testen, ohne sich direkt um die Abhängigkeit kümmern zu müssen. Standardmäßig ist ein Stub zunächst ein Fake.

Betrachten Sie den folgenden Codeausschnitt:

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

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Im vorherigen Beispiel wird ein Stub als Mock bezeichnet. In diesem Fall handelt es sich aber um einen Stub. Sie übergeben „Order“ nur, um Purchase instanziieren zu können (das System, das getestet wird). Die Bezeichnung MockOrder ist ebenfalls irreführend, da auch hier die Reihenfolge keinen Mock darstellt.

Dies ist ein besserer Ansatz:

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

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

Indem die Klasse in FakeOrder umbenannt wird, wird sie generischer. Die Klasse kann als Mock oder Stub verwendet werden, je nachdem, was für den Testfall besser ist. Im Beispiel oben wird FakeOrder als Stub verwendet. Sie verwenden FakeOrder in keiner Form in der Assert-Anweisung. Die FakeOrder wurde nur an die Klasse Purchase übergeben, um die Anforderungen des Konstruktors zu erfüllen.

Zur Verwendung als Mock können Sie z. B. folgenden Code verwenden:

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

purchase.ValidateOrders();

Assert.True(mockOrder.Validated);

In diesem Fall überprüfen Sie eine Eigenschaft des Fakes (d. h. führen eine Assert-Anweisung dafür aus). Im obigen Codeausschnitt ist mockOrder also ein Mock.

Wichtig

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

Der Hauptunterschied zwischen Mocks und Stubs ist also der folgende: Mocks unterscheiden sich im Grunde genommen kaum von Stubs, für diese führen Sie aber eine Assert-Anweisung aus, und für einen Stub nicht.

Bewährte Methoden

Hier sind einige der wichtigsten bewährten Methoden zum Schreiben von Komponententests.

Vermeiden von Infrastrukturabhängigkeiten

Achten Sie beim Schreiben von Komponententests darauf, dass diese nicht von der Infrastruktur abhängig sind. Durch die Abhängigkeiten werden die Tests langsam und fehleranfällig. Diese Abhängigkeiten sollten nur für Integrationstests verwendet werden. Sie können diese Abhängigkeiten in Ihrem Code vermeiden, indem Sie das Prinzip der expliziten Abhängigkeit und Dependency Injection einsetzen. Sie können die Komponententests und die Integrationstests in unterschiedlichen Projekten erstellen. Dieser Ansatz stellt sicher, dass Ihr Komponententestprojekt keine Verweise auf oder Abhängigkeiten von Infrastrukturpaketen aufweist.

Benennen Ihrer Tests

Der Name Ihres Tests sollte aus drei Teilen bestehen:

  • aus dem Namen der Methode, die getestet wird
  • aus den Bedingungen, unter denen getestet wird
  • aus dem erwarteten Verhalten, wenn das Szenario aufgerufen wird

Warum?

Benennungsstandards sind wichtig, da diese die Absicht des Tests explizit ausdrücken. Tests sind nicht nur dazu da sicherzustellen, dass Ihr Code funktioniert, sie bieten auch Dokumentation. Sie sollten anhand der Komponententestsammlung in der Lage sein, das Verhalten Ihres Codes abzuleiten, auch wenn Sie nicht den Code selbst ansehen. Wenn bei Tests Fehler auftreten, können Sie außerdem genau erkennen, welche Szenarios nicht Ihren Erwartungen entsprechen.

Nicht empfohlen:

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

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

    Assert.Equal(0, actual);
}

Empfohlen:

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

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

    Assert.Equal(0, actual);
}

Anordnen Ihrer Tests

Arrange, Act, Assert ist ein häufiges Muster beim Ausführen von Komponententests. Dieses Muster besteht aus drei Hauptteilen:

  • Arrange: Ordnen Sie Ihre Objekte an, erstellen Sie sie nach Bedarf, und richten Sie sie nach Bedarf ein.
  • Act: Führen Sie eine Aktion für ein Objekt durch.
  • Assert: Überprüfen Sie die ordnungsgemäße Funktionsweise.

Warum?

  • Durch diese Methode wird ganz klar unterteilt, was unter den Schritten arrange und assert getestet wird.
  • Es ist weniger wahrscheinlich, dass Überprüfungen mit dem „act“-Code vermischt werden.

Die Lesbarkeit ist einer der wichtigsten Punkte beim Schreiben eines Tests. Durch die Aufteilung innerhalb des Tests werden die Abhängigkeiten deutlich, die beim Codeaufruf, der Art und Weise des Codeaufrufs und bei der Überprüfung entstehen. Einige Schritte könnten zwar möglicherweise kombiniert werden, wodurch die Größe Ihres Tests verringert wird, jedoch liegt das Hauptaugenmerk darauf, den Test besser lesbar zu machen.

Nicht empfohlen:

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

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

Empfohlen:

[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 in einem Komponententest verwendete Eingabe soll so einfach wie möglich gehalten werden, damit das Verhalten, das Sie gerade testen, überprüft werden kann.

Warum?

  • Tests funktionieren auch nach Änderungen an der Codebasis wahrscheinlicher weiter.
  • Das Testverhalten steht im Vordergrund, nicht die Implementierung.

Es ist wahrscheinlicher, dass Tests, die mehr Informationen enthalten, als für das Bestehen erforderlich sind, Fehler verursachen. Dadurch kann die Absicht des Tests weniger eindeutig sein. Wenn Sie Tests schreiben, sollte das Verhalten im Vordergrund stehen. Die Festlegung von zusätzlichen Eigenschaften für Modelle oder die Verwendung von Werten, die nicht 0 (null) sind, wenn es nicht nötig ist, lenkt nur vom gewünschten Ergebnis ab.

Nicht empfohlen:

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

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

    Assert.Equal(42, actual);
}

Empfohlen:

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

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

    Assert.Equal(0, actual);
}

Vermeiden „magischer“ Zeichenfolgen

Das Benennen von Variablen in Komponententests ist so wichtig, wenn nicht noch wichtiger, wie das Benennen von Variablen im Produktionscode. Komponententests sollten keine „magischen“ Zeichenfolgen enthalten.

Warum?

  • Durch magische Zeichenfolgen sieht es der Leser des Tests womöglich nicht als notwendig an, den Produktionscode zu überprüfen, um herauszufinden, was den Wert besonders macht.
  • Sie stellen explizit dar, was Sie beweisen möchten und nicht, was Sie versuchen zu erreichen.

Magische Zeichenfolgen können den Leser Ihrer Tests verwirren. Wenn eine Zeichenfolge nicht wie gewohnt aussieht, fragt sich der Leser womöglich, warum ein bestimmter Wert für einen Parameter oder Rückgabewert ausgewählt wurde. Wegen dieses Zeichenfolgentyps sieht sich der Leser die Implementierungsdetails möglicherweise genauer an und konzentriert sich so nicht mehr auf den Test.

Tipp

Gehen Sie deshalb so genau wie möglich auf die Absicht ein, wenn Sie Tests schreiben. Im Fall von magischen Zeichenfolgen ist es ein guter Ansatz, diese Werte Kontanten zuzuweisen.

Nicht empfohlen:

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

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

    Assert.Throws<OverflowException>(actual);
}

Empfohlen:

[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 Logik in Tests

Wenn Sie Ihre Komponententests schreiben, vermeiden Sie manuelle Zeichenfolgenverkettungen und logische Bedingungen wie if, while, for und switch sowie andere Bedingungen.

Warum?

  • Es ist weniger wahrscheinlich, dass dies zu einem Fehler innerhalb Ihrer Tests führt.
  • Konzentrieren Sie sich auf das Endergebnis und weniger auf die Implementierungsdetails.

Wenn Sie Logik in Ihre Testsammlung einfügen, erhöht sich die Wahrscheinlichkeit, dass dies zu einem Fehler führt. Ihre Testsammlung sollte auf keinen Fall einen Fehler enthalten. Sie müssen sich wirklich sicher sein, dass Ihre Tests funktionieren, andernfalls können Sie sich nicht auf sie verlassen. Tests, denen Sie nicht vertrauen, haben keinen Wert. Wenn ein Test nicht erfolgreich ist, sollten Sie merken, dass tatsächlich etwas mit Ihrem Code nicht stimmt, und dass dies nicht ignoriert werden darf.

Tipp

Wenn Logik in Ihrem Test unvermeidlich scheint, dann erwägen Sie, den Test in zwei oder mehr Tests aufzuteilen.

Nicht empfohlen:

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

Empfohlen:

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

Hilfsmethoden statt SetUp und TearDown

Wenn Sie für Ihre Test ein ähnliches Objekt oder einen ähnlichen Zustand benötigen, nutzen Sie anstelle der Attribute Setup und Teardown (falls vorhanden) lieber eine Hilfsmethode.

Warum?

  • Weniger Verwirrung beim Lesen der Tests, da der gesamte Code innerhalb der einzelnen Tests sichtbar ist.
  • Für den spezifischen Test ist das Risiko geringer, zu viel oder zu wenig einzurichten.
  • Geringeres Risiko, den Zustand zwischen Tests freizugeben, was zu unnötigen Abhängigkeiten zwischen diesen führen würde.

In Komponententestframeworks wird Setup vor jedem Komponententest innerhalb Ihrer Testsammlung aufgerufen. Während einige diese Methode vielleicht als nützlich betrachten, führt sie letztendlich doch zu überfrachteten und schwer zu lesenden Tests. Jeder Test hat in der Regel unterschiedliche Anforderungen, damit er ordnungsgemäß funktioniert. Leider werden Sie mit Setup gezwungen, exakt dieselben Anforderungen für jeden Test zu verwenden.

Hinweis

Ab xUnit Version 2.x sind SetUp und TearDown nicht mehr verfügbar.

Nicht empfohlen:

private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
    stringCalculator = new StringCalculator();
}
// more tests...
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var result = stringCalculator.Add("0,1");

    Assert.Equal(1, result);
}

Empfohlen:

[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var stringCalculator = CreateDefaultStringCalculator();

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

    Assert.Equal(1, actual);
}
// more tests...
private StringCalculator CreateDefaultStringCalculator()
{
    return new StringCalculator();
}

Vermeiden mehrerer Acts

Verwenden Sie beim Schreiben Ihrer Tests nach Möglichkeit nur eine einzelne Act-Anweisung pro Test. Gängige Ansätze zur Verwendung von nur einer Act-Anweisung:

  • Erstellen eines separaten Tests für jede Act-Anweisung
  • Verwenden parametrisierter Tests

Warum?

  • Wenn der Test nicht erfolgreich ist, ist klar, bei welcher Act-Anweisung der Fehler aufgetreten ist.
  • Sorgt dafür, dass sich der Test nur auf einen einzelnen Fall konzentriert.
  • Sie erhalten einen Überblick, warum Ihre Tests fehlschlagen.

Für mehrere Act-Anweisungen müssen jeweils einzelne Assert-Vorgänge ausgeführt werden, und es ist nicht garantiert, dass alle Assert-Vorgänge ausgeführt werden. In den meisten Komponententestframeworks gelten die nachfolgenden Tests automatisch als nicht erfolgreich, sobald ein Assert-Vorgang in einem Komponententest nicht erfolgreich war. Diese Art von Prozess kann verwirrend sein, da eine Funktion, die eigentlich funktioniert, als nicht erfolgreich angezeigt wird.

Nicht empfohlen:

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

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

Empfohlen:

[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 durch Komponententest von öffentlichen Methoden

In den meisten Fällen sollte es nicht nötig sein, eine private Methode zu testen. Private Methoden sind ein Implementierungsdetail und existieren nie isoliert. Irgendwann wird eine öffentliche Methode die private Methode als Teil ihrer Implementierung aufrufen. Sie sollten sich über das Endergebnis der öffentlichen Methode Gedanken machen, die die private abruft.

Betrachten Sie folgenden Fall:

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

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

Möglicherweise versuchen Sie zunächst, einen Test für TrimInput zu schreiben, da Sie sicherstellen möchten, dass die Methode wie erwartet funktioniert. Es ist jedoch auch möglich, dass sanitizedInput von ParseLogLine auf unerwartete Weise manipuliert wird, wodurch das Rendern eines Tests für TrimInput nutzlos ist.

Der echte Test sollte für die öffentliche Methode, ParseLogLine, ausgeführt werden, denn darum sollten Sie sich letztendlich kümmern.

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

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

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

Wenn Sie mit diesem Wissen eine private Methode erkennen, suchen Sie nach der öffentlichen Methode, und schreiben Sie Ihre Tests für diese. Nur weil eine private Methode die erwarteten Ergebnisse liefert, bedeutet das nicht, dass das System, das letztendlich die private Methode aufruft, das Ergebnis richtig verwendet.

Statische Verweise für Stub

Ein Komponententest muss das zu testende System vollständig steuern können. Wenn der Produktionscode Aufrufe von statischen Verweisen (z. B. DateTime.Now) enthält, kann dieses Prinzip zu Problemen führen. Betrachten Sie folgenden Code:

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

Wie kann für diesen Code am besten ein Komponententest ausgeführt werden? Sie könnten beispielsweise folgenden Ansatz ausprobieren:

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

Sie werden wahrscheinlich schnell merken, dass mit Ihren Tests leider einiges nicht stimmt.

  • Wenn die Testsammlung an einem Dienstag ausgeführt wird, ist der zweite Test erfolgreich, der erste schlägt jedoch fehl.
  • Wenn die Testsammlung an einem anderen Tag ausgeführt wird, ist der erste Test erfolgreich, der zweite schlägt jedoch fehl.

Sie müssen zur Lösung dieser Probleme eine Nahtstelle in Ihren Produktionscode einfügen. Eine Methode ist das Umschließen des zu steuernden Codes mit einer Schnittstelle. Der Produktionscode soll dann von dieser Schnittstelle abhängig sein.

public interface IDateTimeProvider
{
    DayOfWeek DayOfWeek();
}

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

Ihre Testsammlung sieht nun so aus:

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

Die Testsammlung hat nun volle Kontrolle über DateTime.Now und kann für jeden Wert einen Stub ausführen, wenn die Methode aufgerufen wird.