Clean Unit-Testing – die Kunst, wartbare Unit-Tests zu schreiben
Unit-Testing ist der erste Weg, um die Qualität von Software möglichst automatisiert sicherzustellen. Aufgrund des Automatisierungsgrades ist Unit-Testing sogar eine der Voraussetzungen für sinnvolle agile Softwareentwicklung. Trotzdem wird das Schreiben ausreichender und sinnvoller Unit-Tests in der Praxis meist vernachlässigt. Warum ist das so? Weil wir als Entwickler:innen unsere Zeit lieber dazu nutzen, neue Features zu entwickeln, als mühsame, repetitive Arbeit in das Erstellen und Warten von Testfällen zu stecken. Features, die jedoch am Ende des Tages bei Anwender:innen für Frustration statt Begeisterung sorgen werden, wenn sie nicht entsprechend getestet wurden.
Das muss aber nicht so sein! Entwickeln Sie doch mit minimalem Zeitaufwand effiziente Unit-Tests, die Ihre tägliche Arbeit unterstützen und erleichtern, anstatt sich mit dem mühsamen Pflegen von Tests ohne großen Mehrwert herumzuplagen! Um Ihnen das zu ermöglichen, zeige ich in diesem Artikel konkrete Best Practices, die erklären, wie Entwickler:innen saubere Unit-Tests schreiben. Der größte Vorteil dieser Praktiken? Mit den richtigen Tools kann man sie mittlerweile sogar automatisieren! Sie glauben mir nicht? Sehr gut – genau deshalb zeige ich am Ende des Artikels anhand eines konkreten Tools die Automatisierungspotenziale, die sich heutzutage bereits ergeben!
Davor gehen wir aber zuerst einmal dem Problem auf die Spur, warum Unit-Testing jetzt eigentlich eine solch mühsame Arbeit ist.
Das Problem mit Unit-Tests
Warum schreiben wir so ungern Unit-Tests? Warum sind Unit-Tests in den meisten Projekten nie auf dem aktuellen Stand und haben deshalb keine Aussagefähigkeit mehr über die Software-Qualität?
Ganz einfach: weil die meisten Unit-Tests einfach schlechte Unit-Tests sind. Es gäbe bereits jahrzehntelang erprobte Best Practices, die Unit-Testing zu einer einfachen und zeitlich effektiv bewältigbaren Aufgaben machen könnten. In der Praxis werden diese Best Practices aber oft nicht verwendet – ganz einfach, weil sie unter Entwickler:innen noch zu wenig verbreitet sind. Stattdessen werden Unit-Tests ad hoc und nach Hausverstand umgesetzt, vielleicht mit ein paar Vorgaben eine:r/s Senior-Entwickler:in. Dadurch steigt der kognitive sowie zeitliche Aufwand für Unit-Testing so sehr an, dass niemand sich mehr damit beschäftigen will.
Was ist ein schlechter Unit-Test?
Aber sehen wir uns einmal konkret an, was einen schlechten Unit-Test ausmacht, der durch Ad-hoc-Entwicklung ohne Verwendung von Best Practices entsteht:
- Er findet keine Fehler. Das manuelle Ableiten von Testfällen aus Anforderungen ist nicht nur schwierig, sondern führt auch oft dazu, dass ineffiziente Fälle getestet werden, die wichtige Fehler womöglich gar nicht finden (selbst Tools wie Code Coverage helfen hier nur bedingt weiter [1]).
- Er erschwert Debugging. Selbst, wenn ein schlechter Testfall einen Bug findet, ist es schwierig, durch Debugging den konkreten Fehler zu beheben. Es dauert sehr lange, überhaupt erst einmal das Ziel jeder einzelnen Codezeile zu verstehen. Selbst, wenn das gelungen ist, macht ein schlechter Aufbau eines Testfalls das Reproduzieren eines Fehlers oft sehr schwer oder sogar unmöglich.
- Er ist schwer wartbar. Der Großteil der Arbeit beim Unit-Testing fällt in der Wartung von Testfällen an. Laufende Änderungen am Code erfordern in der Regel auch das Anpassen mehrerer Unit-Tests. Durch schlechte Codestruktur bzw. Testfallaufteilung ist es sehr schnell sehr aufwändig, bestehende Unit-Tests im Nachhinein anzupassen. Das führt dazu, dass einmal erstellte Tests sehr schnell vernachlässigt werden, da der Aufwand für die Wartung zu hoch wäre.
Um das zu veranschaulichen, geben die Listings 1+2 ein Code-Beispiel.
Listing 1: Code für das durchgängige Beispiel in diesem Artikel. Die Methode MakeChild() erzeugt ein neues Objekt vom Typ Person, basierend auf den übergebenen Vater- und Mutter-Objekten. Dafür verwendet sie die Methode IsFertile(), um zu bestimmen, ob die übergebenen Vater- und Mutter-Objekte fruchtbar sind.
using System;
using System.Collections.Generic;
namespace Example
{
public class Person
{
public enum GenderEnum { men, women }
public String Name { get; set; }
public int Age { get; set; }
public GenderEnum Gender { get; set; }
public Person Father { get; set; }
public Person Mother { get; set; }
public Person (int age, GenderEnum gender)
{
this.Age = Age;
this.Gender = gender;
}
public Person()
{
}
public virtual bool IsFertile()
{
if (this.Gender == GenderEnum.men && Age >= 13 && Age <= 99)
return true;
if (this.Gender == GenderEnum.women && Age >= 12 && Age <= 45)
return true;
return false;
}
/**
** returns a new entity p of type person,
** with p.age = 0, p.father = father and p.mother = mother
** if either father or mother is not fertile, null is returned.
**/
public static Person MakeChild(Person father, Person mother)
{
Person child = new Person();
if (!(father.IsFertile() && mother.IsFertile()))
{
return null;
}
child.Age = 0;
child.Father = father;
child.Mother = mother;
return child;
}
}
}
Listing 2: Zwei beispielhafte Unit-Tests zur Methode MakeChild(), die in diesem Artikel als Negativ-Beispiele verwendet werden.
[Test]
public void initialTest(){
Person father1 = new Person();
father.age = 55;
Person mother1 = new Person();
mother.age = 30;
Person actualChild = Person.createChild()
Assert.AssertEquals(actualChild.getAge(), 0);
Assert.AssertEquals(actualChild.getFather(), father);
Assert.AssertEquals(actualChild.getMother(), mother);
actualChild = Person.createChild(father2, mother2);
Assert.isNull(actualChild);
}
[Test]
public void testTest(){
// Arrange
Mock<Person>father = new Mock<Person>();
Mock<Person>mother = new Mock<Person>();
Random ageGenerator = new Random();
father.age = ageGenerator.next(1,100);
mother.age = ageGenerator.next(1,100);
Person actualChild = null;
// Act
if(father.getAge() >=13 && father.getAge() <= 99 && mother.getAge() >=12 && mother.getAge() <= 45){
actualChild = Person.makeChild(father, mother);
// Assert
Assert.isNotNull(actualChild);
}else{
try{
Person.makeChild(father, mother);
// Assert
Assert.AssertEquals(true, false);
}catch(ArgumentException ex){}
}
}
Sehen Sie sich die Testfälle aus Listing 2 einmal an und stellen Sie sich vor:
- Sie sehen in Ihrer Build-Pipeline, dass einer dieser Testfälle fehlschlägt. Würden Sie nur auf Basis des Namens erkennen, welcher Teil der Software das Problem verursacht? Selbst wenn Sie sich den Code der einzelnen Testfälle ansehen wissen Sie sofort, wo Sie suchen müssen?
- Sie sehen sich den Code der Tests genauer an, um die Ursache des Fehlschlagens des Tests zu finden. Bei Testfall initialTest() können Sie jedoch ohne genaueres Debugging nicht feststellen, ob ein Test aufgrund eines Fehlers in der Methode MakeChild() oder IsFertile() fehlschlägt. Desweiteren gibt es in jeder dieser Methoden mehrere potenzielle Ursachen für ein Fehlschlagen des Tests (falsches Rückgabealter des Kindes, falsches Setzen von mother oder father bzw. falsche Verwendung der Methode IsFertile() innerhalb der Methode MakeChild() bzw. falsche Implementierung der Grenzen von father, mother oder falsche Wertrückgabe innerhalb der Methode IsFertile()).
Beim Testfall testTest() wissen Sie nicht einmal, welche Testdaten den Fehler verursacht haben. Eine erneute Ausführung könnte hier also sogar ein positives Testergebnis liefern, und den eigentlichen Fehler wieder verschleiern. - Die Anforderungen der Methode isFertile() haben sich geändert. Wüssten Sie nun auf die Schnelle, an welchen Stellen Sie den Code für diese beiden Testfälle anpassen müssen? An wie vielen Stellen müssen Sie überhaupt etwas anpassen?
Clean Unit-Testing
Die obigen Beispiele sind typische Probleme, welche Unit-Testing zu einer mühsamen Arbeit machen. Aber zum Glück gibt es mittlerweile erprobte Praktiken, um dem gegenzusteuern – Clean Unit-Testing! Listing 3 zeigt die Anwendung dieser Praktiken auf zwei konkrete Testfälle.
Listing 3: Zwei Beispiel-Unit-Tests für die MakeChild()-Methode aus Listing 1. Diese Tests folgen den Clean-Unit-Test-Code-Prinzipien, die dieser Artikel vorstellt.
[Test]
public void testCreateChild_fatherAndMotherFertile_checkChildCreated(){
// Arrange
Mock<Person>father = new Mock<Person>();
Mock<Person>mother = new Mock<Person>();
father.Setup(t => t.isFertile()).Returns(() => true)
mother.Setup(t => t.isFertile()).Returns(() => true)
Person actualChild = null;
// Act
actualChild = Person.makeChild(father, mother);
// Assert
Assert.isNotNull(actualChild);
}
[Test]
public void testCreateChild_fatherAndMotherFertile_checkChildAgeZero(){
// Arrange
Mock<Person>father = new Mock<Person>();
Mock<Person>mother = new Mock<Person>();
father.Setup(t => t.isFertile()).Returns(() => true)
mother.Setup(t => t.isFertile()).Returns(() => true)
Person actualChild = null;
// Act
actualChild = Person.makeChild(father, mother);
// Assert
Assert.isEqual(actualChild.getAge(), 0);
}
Sehen Sie bereits Vorteile in der Verwendung dieser Tests? Im Folgenden gehen wir auf die zugrundeliegenden Ideen schrittweise ein. Am Anfang steht dabei die Frage:
Welche Unit-Tests soll ich überhaupt erstellen?
Bei der Methode MakeChild() kann zum Beispiel jeder mögliche Integer-Wert für das Alter von Vater bzw. Mutter getestet werden. Für jede mögliche Kombination dieser einzelnen Mutter- und Vater-Objekte muss nun jeweils ein Unit-Test überprüfen, ob die Methode das korrekte Ergebnis zurückliefert.
Da dies natürlich nicht praktikabel ist, muss sich ein:e Unit-Tester:in überlegen, welche Testdaten nun am wahrscheinlichsten einen Fehler in der getesteten Software aufdecken werden. Dafür haben Expert:innen konkrete Techniken entwickelt:
Die Äquivalenzklassen-Partitionierung ermöglicht die Aufteilung (oder auch Partitionierung) des gesamten Werte-Suchraums für Test-Parameter (z. B. alle Integer-Werte für das Alter von father bzw. mother) in Werte-Gruppen (oder auch Klassen), die den selben Pfad in der darunterliegenden Software ausführen (deshalb der Name Äquivalenzklasse).
In die Methode MakeChild() aus Abb. 1 gibt es hier zum Beispiel für eine Person folgende Möglichkeiten:
- Sie ist fruchtbar (bei Männern kann hier jedes Alter zwischen 13 und 99 Jahren verwendet werden).
- Sie ist zu jung, um fruchtbar zu sein (bei Männern 12 Jahre oder jünger).
- Sie ist zu alt, um fruchtbar zu sein (bei Männern zumindest 100 Jahre alt).
- Zusätzlich könnte man noch eine Äquivalenzklasse für ungültige Personen mit negativem Alter nutzen. Da diese Unterscheidung aber nicht in der hier getesteten Code-Unit unterschieden wird (s. Abb. 1), sondern bereits Aufgabe der Klasse Person ist, nur gültige Alterswerte zuzulassen, sind solche Testfälle nicht notwendig.
Um nun möglichst alle fachlich relevanten Gut- bzw. Fehlerfälle abzudecken, sollten Sie zumindest einen Test aus jeder dieser Äquivalenzklassen erstellen. Umgekehrt können Sie fehlende Testfälle in einer bestehenden Test Suite identifizieren, indem Sie für die bereits implementierten Testfälle überprüfen, welche Äquivalenzklassen diese noch nicht abdecken. Die Äquivalenzklassen-Analyse gibt also ein erstes Gefühl dafür, wie viele Tests für eine konkrete Methode sinnvoll sind. Für den ersten Fall in der obigen Äquivalenzklassen-Zerlegung (die Person ist fruchtbar) gibt es aber beispielsweise trotzdem noch 87 Auswahlmöglichkeiten für das Alter (zwischen 13 und 99). Welche dieser Werte sollen Unit-Tests nun konkret abdecken? Zum Glück gibt es auch hier eine Technik, um die effektivsten Daten aus jeder Äquivalenzklasse auszuwählen.
Die Grenzwert-Analyse verwendet die Annahme, dass die meisten Fehler an den Grenzen (die größten und kleinsten Werte) einer Äquivalenzklasse passieren. Zum Beispiel durch Verwendung eines < anstatt <= bei der Prüfung einer Bedingung. Ganz konkret sind das folgende Werte für das Alter von Männern:
- 12 – gerade noch in der Äquivalenzklasse zu jung, um fruchtbar zu sein.
- 13 – gerade noch alt genug, um in der Äquivalenzklasse fruchtbar zu sein.
- 99 – gerade noch jung genug, um in der Äquivalenzklasse fruchtbar zu sein.
- 100 – gerade noch in der Äquivalenzklasse zu alt, um fruchtbar zu sein.
- 0 als untere Grenze der Äquivalenzklasse zu jung, bzw. Interger MAX_Value als obere Grenze der Äquivalenzklasse zu alt, um fruchtbar zu sein.
Somit haben wir aus dem Suchraum aller möglichen Alters-Kombinationen von Vater und Mutter nun durch Anwendung von Äquivalenzklassen-Partitionierung und Grenzwert-Analyse jene 6 Testfälle gefunden, welche am wahrscheinlichsten in Zukunft Fehler aufdecken werden. Doch jetzt wo wir dieses effektive Testset gefunden haben, stellt sich im nächsten Schritt folgende Frage:
Wie schreibe ich Clean Unit-Test-Code?
Denn gerade beim Debugging von Fehlern, bzw. der Wartung von Tests, geht es darum, möglichst schnell zu verstehen, was ein Unit-Test eigentlich macht. Dafür muss der Code möglichst komprimiert und klar strukturiert sein. Um das zu ermöglichen, gibt es verschiedene Praktiken. Diese werden oft unter Akronymen wie FIRST (Fast, Isolated, Repeatable, Self-validating, Thorough [2]) zusammengefasst. Die folgenden Faustregeln geben die Idee hinter diesen Regeln wieder, und können gleich direkt auf Ihre bestehenden Tests angewandt werden:
- Jeder Unit-Test testet exakt eine Code-Unit. Das bedeutet, dass die Ausführung des Testfalls nur ein abgekapseltes Codestück benötigen darf. Dadurch sind Fehler relativ einfach lokalisierbar, da Sie sich beim Fehlschlagen des Tests auf die getestete Code-Unit konzentrieren können. In der Regel ist dabei eine Code-Unit eine Methode. Weitere Code-Units, welche die zu testende Unit für die Ausführung benötigt, müssen durch Mocks oder Stubs gefaket werden [3]. Deshalb übrigens auch der Name Unit-Test. In den Tests aus Abb. 3 wird beispielsweise die Funktionalität der Fruchtbarkeit der Variablen father und mother gemockt. Ziel ist, die Ausführung der Methode MakeChild() von der Methode IsFertile() zu isolieren.
- Jeder Unit-Test überprüft exakt eine Bedingung. Auf Basis des fehlgeschlagenen Testfalls sollte möglichst schnell identifizierbar sein, wo der Fehler liegt. Deshalb sollte auf Basis des Testfalls nicht nur die Code-Unit, sondern auch die Bedingung innerhalb des Codes der zu testenden Unit eindeutig erkennbar sein. Aus diesem Grund gibt es für jede Auswahl an Testdaten (die durch Kombination der Werte der Grenzwert-Analyse entstehen) in der Regel mehrere Unit-Tests, die jeweils verschiedene Bedingungen des selben Code-Pfads prüfen. Abb. 3 zeigt zum Beispiel zwei Unit-Tests, die Daten von zwei fruchtbaren Eltern (aus der Äquivalenzklasse fruchtbar) verwenden, und jeweils unterschiedliche Aspekte des Ergebnisses überprüfen.
- Zwei Durchläufe des selben Unit-Tests auf die selbe Code-Version geben das selbe Ergebnis. Nur so ist die Reproduzierbarkeit von Fehlern auch tatsächlich gegeben. In der Regel ist es hierfür vor allem wichtig, in Unit-Tests niemals Zufallszahlen zu verwenden.
Anhand dieser Faustregeln können Sie nun sinnvollen, wartbaren und debugbaren Unit-Test-Code erstellen. Überprüfen Sie gerne einmal, inwiefern diese Faustregeln bei den Negativbeispielen in Abb. 3 erfüllt sind (oder eben nicht). Doch selbst wenn Sie nun sinnvollen und wartbaren Code geschrieben haben, ist dieser ohne eine einfache und klare Struktur trotzdem noch schwer lesbar. Hierfür hat sich in der Praxis mittlerweile das Arrange-Act-Assert-(AAA)-Pattern als Goldstandard etabliert. Um das AAA-Pattern anzuwenden, strukturieren Sie den Code Ihres Unit-Tests in folgende drei Teile:
- Arrange: Hier bereiten Sie die Code-Unit so vor, dass die zu testende Methode entkapselt getestet werden kann. Außerdem erstellen Sie die Testdaten, die nötig sind, um die relevante Funktionalität zu testen (ausgewählt durch Grenzwert-Analyse).
- Act: In diesem Schritt verwenden Sie nun die vorbereiteten Testdaten, um diese an die zu testende Methode zu füttern. Das von der Methode retournierte Ergebnis können Sie zur Weiterverwendung in einer eigenen Variablen speichern.
- Assert: Hier überprüfen Sie das im vorigen Schritt gespeicherte Ergebnis auf Korrektheit. Dafür bieten Low-level-Unit-Testing-Frameworks wie JUnit oder NUnit verschiedene Assertions an.
Diese einzelnen Teile sind in den Testfällen von Abb. 3 jeweils mit entsprechenden Code-Kommentaren markiert. Es gibt auch weitere Beispiele für dieses Pattern [4].
Fragen Sie sich: Verstehe ich den Testfall, ohne den Code lesen zu müssen?
Die ultimative Hilfe für die Verständlichkeit von Unit-Test-Code ist jedoch folgende: Das richtige Naming der Testfall-Methode. Dafür gibt es konkrete Pattern – ich bringe in diesem Artikel ein Beispiel, das die Idee hinter diesen Pattern aufzeigen soll. Generell ist das Ziel der einzelnen Pattern in der Regel, folgende Informationen in der Klasse sowie dem Namen jedes Testfalls einheitlich zu hinterlegen:
- Die getestete Code Unit (Methode)
- Die verwendeten Testdaten
- Die überprüften Bedingungen
Angewandt auf einen konkreten Unit-Test aus Abb. 3: testCreateChild_fatherAndMotherFertile_checkChildAgeZero zeigt zuerst den Namen der Code-Unit, die getestet wird (die Methode CreateChild), gefolgt von den verwendeten Testdaten (die Methoden-Parameter father und mother sind jeweils fruchtbare Person-Objekte) und schließlich der zu überprüfenden Bedingung (der Assert-Teil des Tests überprüft, ob das von der Methode CreateChild() zurückgegebene Objekt ein Alter von 0 hat). Die selbe Regel ist auch auf den zweiten Testfall testCreateChild_fatherAndMotherFertile_checkChildCreated angewandt.
Unabhängig vom konkreten Pattern zum Finden eines Namens hilft folgende Faustregel, einen guten Testfallnamen ausfindig zu machen. Fragen Sie sich: "Verstehe ich den Testfall, ohne den Code lesen zu müssen?" Denn das ist der eigentliche Grund, warum Sie überhaupt ein Naming-Pattern für Unit-Tests verwenden sollen. Wenn der Test fehlschlägt, können Sie sofort einen Blick auf das getestete Codestück werfen, ohne dafür zuerst den Test-Code verstehen zu müssen.
Automatisierung mit der richtigen Tool-Unterstützung
Die Anwendung der in diesem Artikel vorgestellten Praktiken hilft nun, mit möglichst wenig Zeitaufwand effektive und wartbare Unit-Tests zu erstellen. Diese Praktiken immer und immer wieder anzuwenden, ist jedoch ebenfalls repetitiv und mühsam. Es gibt Faustregeln und Checklisten, die dabei helfen, Clean Unit-Testing so einfach wie möglich zu praktizieren [5]. Wenn Sie in Ihrem Projekt jedoch noch einen Schritt weiter gehen wollen, dann automatisieren Sie doch ganz einfach die Anwendung der Clean-Unit- Testing-Best-Practices, anstatt sich selbst um ihre Einhaltung kümmern zu müssen. Moderne Unit-Testing-Tools erlauben die High-Level-Spezifikation von Testfällen durch grafische Oberflächen. Diese Oberflächen führen Entwickler:innen schrittweise durch relevante Best Practices, um so mit möglichst wenig Aufwand relevante Testfälle zu identifizieren. Den Code für die nötigen Testfälle erstellt das Tool anschließend automatisch aus den über die Oberfläche eingegebenen Informationen.
Anwendung mit Devmate
Als Beispiel hierfür zeigt dieser Artikel die äquivalenzklassen-basierte Testfall-Generierung mit dem Testing-Tool Devmate [6]. Sobald dieses Tool für die IDE Ihrer Wahl (in unserem Fall Visual Studio 2022) installiert ist, erstellen Sie den nötigen Test-Code für jede beliebige Methode mit folgenden 4 Schritten:
- Starten des Testing-Tools über die IDE: Der obere Teil von Abb. 4 zeigt, wie Sie über das Aufrufen des Kontextmenüs im Code der zu testenden Methode den Testing-Prozess starten.
- Äquivalenzklassen-Erstellung: Ein Klick auf den Button "Test with Devmate" im Kontextmenü öffnet nun die in Abb. 5 gezeigte Benutzeroberfläche. Dort legen Sie für jeden Methodenparameter die entsprechenden Äquivalenzklassen fest. Sie können dabei neue Äquivalenzklassen definieren oder auf bereits bestehende Äquivalenzklassen für ähnliche Parameter auf Basis eines sogenannten Recommender-Systems zugreifen. In beiden Fällen besteht jede Äquivalenzklasse aus einem Namen sowie einer Liste von Repräsentanten. Ein Repräsentant ist ein konkreter Wert, den der Parameter annehmen kann, um die gewählte Äquivalenzklasse zu repräsentieren. Abb. 5 zeigt hier als Beispiel die Äquivalenzklassen für unser isFertile-Beispiel.
- Testfall-Generierung: Durch Kombination der Äquivalenzklassen für alle verfügbaren Input-Parameter der zu testenden Methode erstellt das Tool nun automatisiert Testfälle, um die entsprechende Methode fachlich ausreichend zu testen. Der untere Teil von Abb. 4 zeigt das entstandene Ergebnis für unser isFertile-Beispiel (für diese simple Methode gibt es lediglich den Parameter this, der als Zeiger auf das Objekt dient, auf welches der Test-Code die Methode isFertile() aufrufen soll). Durch Auswahl der Repräsentanten für jede Äquivalenzklasse (z. B. auf Basis der Grenzwert-Analyse), sowie der Eingabe des zu überprüfenden Verhaltens für jeden Testfall (durch Assertions auf das Rückgabe-Objekt, Seiteneffekte oder erwartete Exceptions) und eines sprechenden Namens für jeden Testfall (entsprechend eines Clean-Unit-Test-Code-Naming-Patterns) entsteht somit das in Abb. 4 gezeigte Bild.
- Testcode-Generierung: In Abb. 4 sind nun alle nötigen Informationen festgelegt, die für methodisch sauberen Unit-Test-Code benötigt werden. Durch Klick auf den Button "Generate NUnit Test Code" in der Benutzeroberfläche kann das Tool nun auf Basis dieser Informationen den Code für alle definierten Tests automatisch erzeugen. Das Beste daran: der erstellte Code entspricht natürlich auch automatisch sämtlichen Best Practices bzgl. Struktur, Entkapselung und Naming (s. Abb. 3) und ist in bestehenden Unit-Test-Code-Frameworks integriert, um z. B. die Erstellung von Assertions zu vereinfachen (in diesem Beispiel das C#-Unit-Testing-Framework NUnit). Beispiele für solchen Code haben wir bereits in Abb. 3 gesehen.
Diese 4 Schritte geben einen fast vollständig automatisierten Prozess vor, in dem sich die Arbeit von Entwickler:innen auf das wesentliche Minimum reduziert. Doch sogar diese übrig gebliebene Arbeit wird durch den Einsatz effizienter Methoden wie Äquivalenzklassen-Partitionierung, Grenzwert-Analyse sowie einem standardisierten Naming-Pattern von Unit-Tests, wie in diesem Artikel vorgestellt, weiter reduziert.
Fazit
Nach dem Lesen dieses Artikels sollte Ihnen nun klar sein, warum Unit-Testing bisher für Sie eine mühsame Arbeit war, deren Nutzen nur selten den nötigen Aufwand rechtfertigte. Doch zum Glück haben Sie nun konkrete Faustregeln zur Hand, um in Zukunft Clean Unit-Testing praktizieren zu können. Dadurch reduziert sich nicht nur der kognitive und zeitliche Aufwand für Unit-Testing, sondern Ihre Tests helfen Ihnen auch endlich, tatsächliche Fehler in der Software zu finden.
Falls Ihnen die Anwendung von Clean Unit-Testing selbst zu mühsam erscheint, haben wir auch hierfür die perfekte Lösung gezeigt: Lassen Sie sich mit dem richtigen Tool ganz automatisch saubere Unit-Tests erzeugen! Diese Tools bieten bereits heute enormes Potenzial, den Alltag von Entwickler:innen angenehmer zu machen. Dieses Potenzial wird sich in Zukunft jedoch noch vervielfachen, sobald immer mehr Techniken aus dem Bereich der Künstlichen Intelligenz Einzug ins Software-Testing halten.
- D. Lehner: Limits at which code coverage fails catastrophically
- D. Newton: Writing Your F.I.R.S.T Unit Tests
- M. Fowler: Mocks Aren't Stubs
- D. Lehner: The AAA Test Pattern explained with C# and NUnit
- D. Lehner: The Ultimate Checklist for Better Unit Testing
- Devmate Software