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?
Wie bereits in meinem letzten Artikel erwähnt, hilft mir TDD, die Qualität des Codes sicherzustellen und zu gewährleisten, dass alle Anforderungen einer Aufgabe erfüllt werden. Es hat lange gedauert, bis ich wirklich verstanden habe, wie TDD funktioniert, weil ich kein gutes Beispiel dafür hatte, wie der Prozess wirklich funktioniert. Deshalb möchte ich es heute mit Ihnen teilen.
Meine Reise mit TDD begann mit dem folgenden Video, in dem Robert C. Martin den Prozess demonstrierte:
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.
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.
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
Aber jetzt gibt die IDE einen Fehler aus, der besagt, dass sie das Symbol "MyStack" nicht auflösen konnte. Hier beginnt der Zyklus. Erinnern Sie sich an den letzten Artikel erwähnt indem ich über TDD mit den drei Phasen "rot", "grün" und "refactor" geschrieben habe. Jetzt verstehen Sie vielleicht, warum die erste Phase "rot" genannt wird: Wir schreiben Code, bis wir einen Fehler finden. Das ist der Moment, in dem wir ihn beheben müssen.
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.
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 😉.
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.
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.
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.