TDD in der Praxis: Wie Sie es konkret anwenden können

Möchten Sie die Codequalität Ihrer Softwareentwicklungsprojekte verbessern? Testgetriebene Entwicklung (TDD) ist ein bewährter Ansatz, mit dem Sie dieses Ziel erreichen können. In dieser Schritt-für-Schritt-Anleitung führe ich Sie durch den Prozess der Implementierung von TDD in Ihren Projekten, von der Erstellung grundlegender Tests bis zum kontinuierlichen Code-Refactoring. Tauchen wir ein!

Warum mache ich TDD?

As mentioned in my last article, TDD helps me to ensure the code quality and that all requirements of a task are fulfilled. It took me a long time before I really understood how TDD works, because I haven’t had a good example how the process really works. That’s why I want to share it with you today.

Meine Reise mit TDD begann mit dem folgenden Video, in dem Robert C. Martin den Prozess demonstrierte:

Robert C. Martin zeigt, wie TDD funktioniert (ab 43:48 min)

Grundlegende Tests erstellen

Da mir das Beispiel von Uncle Bob gefällt, um TDD bei der Entwicklung eines Stacks zu zeigen, werde ich dies auch als Beispiel nehmen. Meine Implementierung wird in C# sein und xUnit als Test-Framework verwenden. Wenn Sie mit xUnit nicht vertraut sind, schauen Sie sich zunächst die Dokumentation an. Zunächst müssen Sie sicherstellen, dass Sie eine Testumgebung haben. Das heißt, Sie haben ein Anwendungsprojekt und ein Testprojekt, das eine Referenz auf das Anwendungsprojekt hat. Wenn der Test ausgeführt werden kann, ist die Testumgebung funktionsfähig. Um eine solche Umgebung zu schaffen, erstelle ich eine neue Projektmappe mit einer Konsolenanwendung und verweise das Konsolenanwendungsprojekt auf das Unit-Test-Projekt.

JetBrains Rider Project Explorer zeigt das Konsolenanwendungsprojekt und das Unit-Test-Projekt, das eine Referenz enthält.
Verbinden Sie das Unit-Test-Projekt mit der Konsolenanwendung

Die C#-Klasse StackTest enthält einen einfachen Test, der nichts tut und nur dazu dient, zu überprüfen, ob der Test-Runner ausgeführt werden kann.

namespace TddDemo.UnitTests;

public class StackTest
{
    [Fact]
    public void Nothing()
    {
    }
}

Schließlich müssen Sie nur noch die Tests ausführen und sehen, ob sie funktionieren oder ob es Probleme gibt.

JetBrains Rider Test Runner zeigt einen einzelnen Unit-Test namens "Nothing" an, der erfolgreich ausgeführt wurde.
Prüfen Sie im Test-Runner Ihrer IDE, ob der Test erfolgreich ausgeführt wurde

Der zweite Schritt bei der Implementierung von TDD besteht darin, einen grundlegenden Test zu erstellen, der eine bestimmte Funktionalität Ihres Programms überprüft. Dieser Test dient als Vorlage für den Code, den Sie schreiben werden. Wenn Sie mit einem Test beginnen, haben Sie eine klare Vorstellung davon, was Ihr Code leisten soll. Aber wo soll man anfangen? Hierfür gibt es eine einfache Regel:

Sie schreiben den Test, der Sie dazu zwingt, den Code zu schreiben, von dem Sie bereits wissen, dass Sie ihn schreiben wollen.

Robert C. Martin alias “Uncle Bob”

Wir müssen also zunächst mit etwas so Einfachem wie dem Schreiben eines Tests beginnen, der einen Stack erzeugen soll.

    [Fact]
    public void CreateStack()
    {
        var myStack = new MyStack();
    }

Implementierung des Minimalcodes

But now the IDE throws an error, saying that it could not resolve the symbol “MyStack”. That’s where the cycle begins. Remember the last article I wrote about TDD with the three phases “red”, “green” and “refactor”. Now, you maybe see why the first phase is called “red”: we write code until we get an error. That’s the moment where we have to fix it.

Ziel ist es, einen möglichst einfachen Code zu schreiben, der die Anforderungen des Tests erfüllt. Indem Sie sich darauf konzentrieren, den Test zu bestehen, stellen Sie sicher, dass Ihr Code von Anfang an korrekt funktioniert. In diesem Fall ist es einfach, eine neue Klasse namens "MyStack" im Anwendungsprojekt zu erstellen.

JetBrains Rider mit einer neuen Klasse namens "MyStack" im Projekt "TddDemo", das in diesem Fall unsere Anwendung ist.
Die Erstellung einer Klasse löst das Problem

Wenn Sie nun zu Ihrem Testfall zurückwechseln, sehen Sie, dass der Fehler verschwunden ist. Wenn Sie den Test jetzt ausführen, ist er grün. Unsere "grüne" Phase ist also auch abgeschlossen.

Kontinuierliches Code-Refactoring

Nach bestandenem Test ist der nächste Schritt das kontinuierliche Refactoring des Codes. Dabei geht es darum, die Qualität Ihres Codes zu verbessern, ohne dessen Funktionalität zu beeinträchtigen. Durch regelmäßiges Refactoring Ihres Codes können Sie alle technischen Schulden beseitigen und sicherstellen, dass Ihre Codebasis sauber und wartbar bleibt. Mir persönlich gefällt der Name "MyStack" nicht, also werde ich die Klasse in "Stack" umbenennen. Nachdem Sie die Klasse umbenannt haben, führen Sie den Test erneut durch, um zu sehen, ob er immer noch erfolgreich ist.

Den Prozess fortsetzen

Das vorliegende Beispiel war nur der Anfang des TDD-Prozesses und daher etwas simpel. Außerdem überprüft der Test nichts. Um Ihnen einen besseren Einblick zu geben, füge ich dem Code weitere Logik hinzu, um Ihnen zu zeigen, wie Sie den Prozess fortsetzen können. Aber lassen Sie uns zuerst unseren ersten Test abschließen. In diesem Fall ist es sinnvoll, zu prüfen, ob der neu erstellte Stack leer ist. Fügen wir also diese Prüfung in den Test ein.

    [Fact]
    public void CreateStack()
    {
        var myStack = new Stack();
        Assert.True(myStack.isEmpty());
    }

Die IDE zeigt wieder einen Fehler an. Denken Sie daran: Jetzt sind wir wieder in der "roten" Phase - korrigieren Sie den Code. Die Lösung besteht in diesem Fall darin, die Methode hinzuzufügen. Ich lasse die IDE hier die Hauptarbeit für mich erledigen 😉.

Verwendung des JetBrains Rider Suggestion Tools, um der Klasse "Stack" die fehlende Methode "isEmpty" hinzuzufügen.
Hinzufügen der Methode mit Hilfe der IDE-Tools

Führen Sie den Test jetzt aus, und er wird rot sein. Dieser Schritt ist wichtig, um zu sehen, ob unser Test wirklich funktioniert oder nur immer grün wird.

JetBrains Rider Unit Test Explorer zeigt einen Fehler im Test an.
Die Ausnahme "Methode nicht implementiert" wurde erwartet - jetzt wissen wir, dass unser Test funktioniert

Um das Problem zu beheben, müssen Sie einen möglichst einfachen Code schreiben, um den Test zu erfüllen. In diesem Fall wird einfach true zurückgegeben, wenn die Methode aufgerufen wird. Und der Test läuft wie geschmiert.

"Aber warum sollte ich das so machen?", werden Sie sich vielleicht fragen. Mit dieser Methode sehen Sie, dass Ihr Test erfolgreich und nicht erfolgreich ist, und Sie brauchen fast keine Zeit, um Ihren Test zu testen.

Jetzt ist es Zeit für ein Refactoring. Zunächst würde ich die Methode umbenennen, um den Namenskonventionen zu entsprechen (von "isEmpty" in "IsEmpty"). Testen Sie erneut und benennen Sie anschließend die Testmethode um (von "CreateStack" in "NewStackIsEmpty").

Um den Prozess fortzusetzen, erstelle ich einen Test, um etwas in den Stack zu schieben. Ich bin so weit gekommen, bevor die IDE einen Fehler anzeigt:

    [Fact]
    public void PushInStack()
    {
        var myStack = new Stack();
        myStack.Push(5);
    }

Auch hier ist die Methode nicht vorhanden. Fügen wir sie also hinzu und führen wir den Test durch, um zu sehen, was los ist.

JetBrains Rider: Code der Push-Implementierung und rotes Testergebnis, das eine Ausnahme "nicht implementierte Methode" zeigt.
Die Ausnahme wurde nicht erwartet, daher wird ein Fehler angezeigt

Entfernen Sie die NotImplementedException und der Test wird grün - Problem gelöst(?). Beim Refactoring benenne ich den Parameter "i" in "element" um. Es funktioniert immer noch, keine Überraschung so weit.

Wieder brauche ich eine Prüfung (Assertion). Also wird angenommen, dass "myStack.IsEmpty()" false zurückgibt. Und der Test wird wieder rot. Jetzt müssen wir das Problem beheben.

Eine der Regeln der testgetriebenen Entwicklung lautet, dass Sie so wenig Gehirnzellen wie möglich beanspruchen, denn Sie werden sie später brauchen. Machen Sie nicht zu viel zu schnell.

Robert C. Martin alias “Uncle Bob”

Um diese Regel einzuhalten, erstellen wir in der Klasse eine Variable mit dem Namen "isEmpty", die mit "false" initialisiert wird und auf "true" wechselt, wenn die Methode "Push" aufgerufen wird.

public class Stack
{
    private bool _isEmpty = true;
    
    public bool IsEmpty()
    {
        return _isEmpty;
    }

    public void Push(int element)
    {
        _isEmpty = false;
    }
}

Beim Refactoring würde ich die doppelte Initialisierung von "myStack" entfernen, so dass der Code für die Testklasse wie folgt aussieht:

public class StackTest
{
    private readonly Stack _myStack = new();

    [Fact]
    public void NewStackIsEmpty()
    {
        Assert.True(_myStack.IsEmpty());
    }

    [Fact]
    public void PushInStack()
    {
        _myStack.Push(5);
        Assert.False(_myStack.IsEmpty());
    }
}

Der Prozess würde jetzt immer so weitergehen.

Herausforderungen, Gründe und Lösungen

Auf den ersten Blick sieht es seltsam aus. Warum diese winzigen Babyschritte? Warum wird nicht zuerst der Code implementiert? Wenn Sie Ihre Logik zuerst schreiben würden, würde dies wahrscheinlich zu einem nicht testbaren Code führen, der nicht modular ist. Das würde die mögliche Testabdeckung verringern. Die kleinen Schritte sind wichtig, um sicherzustellen, dass die Tests tatsächlich funktionieren. Sonst könnte es passieren, dass ein Fehler auftritt, obwohl der Test grün ist, was zu unzuverlässigen Tests führt. Das ist ein großes Problem, das Sie unbedingt vermeiden wollen.

Aber es gibt auch Vorteile: Die Anwendung dieser Technik reduziert die Menge an geistiger Arbeit, die Sie für die Erstellung von Code benötigen, da Sie nur Tests erfüllen. Außerdem haben Sie viele Erfolgserlebnisse. Jeder grüne Test ist ein Erfolg für sich. Werden Sie nicht süchtig 😉 Zudem können Sie sicher sein, dass der mit TDD erstellte Code so funktioniert, wie in den Tests definiert. Das reduziert den Stress und die Anzahl der notwendigen manuellen Tests.

Je mehr Erfahrung Sie mit TDD sammeln, desto schneller werden Sie. Vielleicht entscheiden Sie sich, einige Setup-Schritte zu überspringen, aber Sie sollten es zuerst ausprobieren.

TDD in unterschiedlichen Programmiersprachen

TDD ist ein unabhängiger Ansatz, der in fast jeder Programmiersprache angewendet werden kann. Egal ob Sie mit JavaScript, Python, Java oder einer anderen Sprache arbeiten, TDD kann effektiv implementiert werden. In diesem Artikel habe ich ein Beispiel mit C# und xUnit gezeigt.

Wenn Sie TDD in Ihren Entwicklungsprozess integrieren möchten, aber nicht wissen, wo Sie anfangen sollen, bin ich für Sie da. Ich kann Sie durch den Prozess der Implementierung von TDD führen und Ihnen helfen, eine hervorragende Codequalität zu erreichen.

Schreibe einen Kommentar