Warum die besten Entwickler nie auf automatisierte Tests verzichten

Autor: Sergej Kukshaus Veröffentlicht: vor 2 monaten Lesedauer: ca. 11 Minuten

Kennst du das auch?

Du hast gerade ein neues Feature in dein Projekt implementiert und es funktioniert alles wunderbar. Das Feature ist komplett fehlerfrei und du bist total zufrieden mit der Umsetzung.

Der Quellcode sieht sehr gut aus und lässt sich sehr gut lesen.

Im Anschluss erstellst du für deine Applikation ein Setup und suchst dir einen geeigneten Tester.

Und schon passiert es.

Auf einmal funktionieren andere Features nicht. Features, die schon seit langem funktionierten und bei denen du ansonsten auch nichts verändert hast.

Oder noch schlimmer:

Die Fehler fallen gar nicht auf. Die Applikation wird released und nach zwei Wochen meldet der Kunde den Fehler. Im besten Fall stürzt das Programm ab.

Das worst-case Szenario wäre: Die Applikation stürzt nicht ab, sondern generiert inkonsistente Daten.

Nach einer wahren Begebenheit…

Das kennt man eigentlich nur aus Filmen. Jedoch möchte ich dir etwas erzählen, was weder lustig, noch frei erfunden ist.

Ein Unternehmen, welches eine Kundenverwaltungssoftware für einen kleinen Nischenmarkt vertrieben hat, ist irgendwann mal an den gekommen, an dem genau das obere  Szenario eingetreten ist.

Mit jeder publizierten Version kamen immer mehr und mehr Fehler hinzu. Die Entwickler haben schon längst den Überblick verloren und jedes neue Feature kostete immer mehr und mehr Zeit.

Selbst diverse Bugfixes kosteten mehr Zeit als die Implementierung des eigentlichen Featuers.

Das Unternehmen wurde immer unproduktiver. Bugfixes sind schließlich eigenes verschulden und als gutes Unternehmen macht man diese natürlich auch kostenfrei.

Jetzt hat dieses Unternehmen in einer Version ganz viele Bugs „gelöst“. 

Wie? Na, indem die Fehlermeldungen einfach dem Kunden nicht mehr gezeigt wurden. Der Kunde dachte, es wäre alles in Ordnung.

Weit gefehlt. Die Daten wurden falsch und unvollständig gespeichert. Sogar Bestandsdaten wurden verändert.

Natürlich war das auch für die Kunden ein wirtschaftlicher Verlust. Es mussten Daten korrigiert werden und manuell abgeglichen werden.

Ein heiden Aufwand!

Und die Moral von der Geschicht‘? Die Firma, von der ich rede, die gibt es heut nicht!

Warum? Der Kundenkreis, waren Anwälte. Und da wundert es mich nicht, dass eine Klage nach der anderem reinkam und die Firma jetzt insolvent ist.

Und ganz ehrlich, soweit muss es gar nicht kommen.

Lassen sich Fehler vermeiden?

Wir sind doch alle nur Menschen. Menschen sind keine Maschinen und machen irgendwann mal Fehler. Da wirst du mir wahrscheinlich recht geben, oder?

Doch was hätten die Programmierer besser, bzw. anders machen können?

Ein guter Vorsatz in der Softwareentwicklung ist:

Fehler möglichst früh erkennen

Menschen machen Fehler. Maschinen nicht. Vorausgesetzt, man hat es der Maschine richtig erklärt. In diesem Fall kann die Maschine die gleiche Arbeit auch immer und immer wiederholen. 

Eine Maschine fragt nicht: „Warum?“ Eine Maschine sagt nur: „Jawohl Meister!“

Die Maschine würde auch viel schneller reagieren als der Mensch. Warum? Weil der Mensch denkt. Eine Maschine kann nicht denken.

Selbst in einer Zeit von „künstlicher Intelligenz“ und „Maschine learning“. Eine Maschine denkt nicht. Sie tut genau das, was der Mensch ihr beigebracht hat. Nicht mehr und nicht weniger.

Deswegen heißt es ja auch künstiliche Intelligenz. Das hat auch nichts mehr mit Kunst zu tun 🙂

Es werden vordefinierte Algorithmen ausgeführt. Immer und immer wieder. Das „Gehirn“ der Maschine ist entweder der Arbeitsspeicher oder die Festplatte.

Wie lassen sich Fehler früh erkennen?

Fassen wir mal kurz zusammen:

Eine Maschine…

  • … macht immer die gleiche, vordefinierte Arbeit.
  • … fragt nicht nach Sinn und Unsinn.
  • … ist schneller als der Mensch.
  • … braucht keine Pause.
  • … beurteilt objektiv.
  • … und vieles mehr 🙂

Was bringt uns das?

Anstelle jemanden einzustellen, der immer wieder die Software testet, kann man das doch von einer Maschine erledigt lassen.

Sagen wir mal, wir haben ein Testhandbuch mit ca. 74821 Festfällen. 

Was meinst du, wie lange würde ein Mensch daran sitzen? Ich würde behaupten, dass es im besten Fall ca. zwei Monate dauert.

Und eine Maschine? Dann lass es mal im schlimmsten Fall zwei Stunden dauern. 

Wobei wir schon an dem Punkt wären.

Den automatisierten Tests.

Was sind automatisierte Tests?

Im Grunde genommen, lässt sich das in einem Satz erklären:

Ein Mensch drückt auf ein Knopf und die Maschine durchläuft das komplette Testhandbuch mit den 74821 Festfällen und gibt an, welche Features fehlerbehaftet sind.

Natürlich kann man auch nur ein Bruchteil aus diesem Handbuch testen lassen. Man möchte schließlich nicht bei der kleinsten Änderung zwei Stunden auf die Testergebnisse waren, die sowieso fehlerfrei funktionieren.

Spätestens jedoch vor einem Release sollten alle Tests ausgeführt werden. Am besten jedoch mindestens einmal am Tag. 

Wo wir wieder beim Stichwort wären: Fehler möglichst früh erkennen.

Automatisierte Tests ersetzen nicht manuelle Tests.

Der Aufwand für manuelle Tests wird jedoch auf ein minimum reduziert.

Automatisierte Tests, bezeichnet man in der Praxis auch als Unit-Test. Auch, wenn es eigentlich kein Unit-Test mehr ist. Doch dazu später mehr 🙂

Welche Nachteile bergen automatisierte Tests (Unit-Tests)

Ein Unit-Test-Projekt steht und fällt mit der Qualität der Unit-Tests.

Wenn der Test schlecht – oder gar falsch formuliert – ist, dann ist dieser komplett nutzlos.

Viele Entwickler finden die Unit-Tests ziemlich nervig und schreiben diese erst gar nicht. Kein Wunder: Unit-Tests zu schreiben benötigt Zeit. Viel Zeit. 

Und wenn sich mal eine Anforderung ändert, dann müssen auch die Unit-Tests angepasst werden, damit das wiederum wieder passt.

In einem guten Unit-Tests-Projekt stellt sich häufig die Frage: 

Habe ich ein Bug eingebaut oder hat sich die Anforderung geändert?

Man muss also auch mal kritisch überlegen, ob das so richtig ist, was man da gebaut hat oder ob die aktuelle Anforderung nicht mehr passt. Wenn dies der Fall ist, sollte man aber nicht einfach den Test ändern. Nein, am besten ist es, wenn man mal bei seinen Kollegen nachfragt bevor man da etwas ändern. Kann sich ja doch ein Fehlerteufel eingeschlichen haben 🙂

Welche Vorteile haben automatisierte Tests

Wer an einem größeren Projekt arbeitet, bei dem keine (oder nur wenig) Unit-Tests vorhanden sind, wird es kennen:

Die Testphase dauert lange. Extrem lange. Teilweise genauso lange wie die Entwicklungsphase.

Aber selbst da können mal Fehler durchgehen und eine fehlerbehaftete Version auf den Markt kommen.

Du wirst schneller

Man benötigt zwar Zeit, um den Test zu schreiben, jedoch sollte man sich diese Zeit nehmen. Schließlich kann es sogar bereits beim Entwickeln bereits Zeit sparen.

Kaum hat man eine neue Funktion geschrieben, führt man das Programm aus, navigiert zu der Maske an der man gerade arbeitet und gibt da irgendwelche Testdaten ein.

Je nach Größe des Programms kann dieser Vorgang mitunter mehrere Minuten Zeit in Anspruch nehmen. 

Der Unit-Test würde diesen Aufwand auf ein paar Sekunden reduzieren.

Der eigentliche Testaufwand würde sich auch auf ein paar Minuten reduzieren.

Kleines Beispiel hierzu:

Ich habe mal ein Programm geschrieben, bei dem der Zeitpunkt zwischen dem Kompilieren und der Auslieferung weniger als eine Stunde vergeht. Das Programm beinhaltet ca. 65 Tausend Zeilen Code. Es ist also nicht sehr groß, aber auch nicht mehr so klein. 

Du wirst mutiger

Ein nicht zu verachtender Vorteil ist bei diesem Vorgehen, dass man mutiger wird.

Mutiger, einfach mal neue Features hinzuzufügen.

Aber auch mal etwas Code zu Refaktorisieren. Teilweise auch mal vorhandene Klassen komplett neu zu schreiben.

Dies sollte man immer mal wieder machen, wenn der eigene Quellcode nicht mehr ganz den Richtlinien entspricht und z.B. teile des Quellcodes ausgelagert werden sollen.

Du schreibst besseren Code

Ob du es glaubst oder nicht. Wenn man Unit-Tests streng nach dem TDD-Prinzip schreibt, schreibt man automatisch besseren Code.

Ein Unit-Test soll nach Möglichkeit die komplette Methode abdecken. Es soll jede Möglichkeit getestet werden. Vor allen sollen „Edge-Cases“ berücksichtigt werden. Je komplexer die Methode ist, desto mehr Tests werden benötigt, um alle Testfälle abzudecken.

Glaub mir, nach ein paar Tests hat man einfach kein Bock mehr noch mehr in diese eine Methode zu packen und schreibt einfach eine neue. 

Für eine Methode, die man vorher hätte in ca. 100 Zeilen Code geschrieben, schreibt man nach dem TDD-Prinzip jetzt 10 Methoden mit jeweils 5 bis 20 Zeilen Code.

Deine Klassen werden automatisch kleiner

In der Tat ist das so, dass man sich mehr um Anforderungen auseinandersetzt.

Bevor der Code geschrieben wird, denkt man als Entwickler zunächst „was möchte ich erreichen“ und vor allem „wie bewerkstellige ich das am einfachsten“.

Es sollte nicht das Ziel sein möglichst viel Quellcode zu produzieren. Stattdessen sollte man es schaffen möglichst viel Funktionalität in so wenig Quellcode abzubilden wie möglich und dabei stets wartbaren Code zu schreiben.

Wir sind schließlich alle nur Menschen. Menschen denken einfach. Keiner mag es unnötig nachzudenken.

Wie der Programmierer-Guru Steve Krug es bereits treffend in seinem Buch formuliert hat:

Steve Krug: Don't Make Me Thing

Kleine Klassen sind einfach übersichtlicher. Kleine Methoden sind übersichtlicher. Man sieht auf den ersten Blick was genau in der Methode oder der Klasse passiert.

Ich als Leser, bin nicht mehr dazu gezwungen viel nachzudenken. Jedenfalls nicht, um den Quellcode zu verstehen. Ich kann mich damit darauf konzentrieren das eigentliche Problem zu verstehen und nicht den Quellcode.

Wie lassen sich automatisierte Tests erstellen?

Wenn du die letzten ca. 1000 Wörter übersprungen hast, dann „welcome back“ 🙂 

Jetzt geht es nämlich sehr konkret weiter.

Automatisierte Tests in es in mehreren – nennen wir es mal – Stufen.

Eine Art hiervon nennt man „Unit Test“. Das ist auch die häufigste Art von automatisierten Tests.

Nach der Ausführung sind Tests entweder Rot (fehlerbehaftet) oder Grün (erfolgreich). Klingt doch logisch, oder?

In der Vergangenheit haben sich zwei ganz spezielle Muster bei Unit-Tests bewährt.

RGR-Pattern: Red-Green-Refactor

TDD-Cycle: Red Green Refactor

Heißt nichts anderes als:

  1. Erstelle einen Test. 
  2. Red: Mach es so früh wie möglich „rot“. Im einfachsten Fall geht das, wenn man eine neue Funktion hinzufügt, die es im vorfeld noch nicht gab.
  3. Green: Mach den Test so einfach und schnell wie nur möglich „grün“.
  4. Refactor: Schreib den Quellcode vernünftig.
  5. Wiederhole Schritt 1 bis 4 so lange bis du mit dem Quellcode zufrieden bist.

Und das ist schon das ganze Geheimnis des beliebten TDD-Musters (Test-Driven-Development).

AAA-Pattern: Arrange-Act-Assert

Doch wie baut man eigentlich so ein UnitTest auf?

Auch hierfür hat sich in der Vergangenheit ein Muster herauskristalisiert.

  • Arrange: Erstelle alles, was du benötigst um eine Funktion zu testen.
  • Act: Führe die Funktion aus (hier steht nur eine Zeile)
  • Assert: Prüfe ob das Ergebnis richtig ist. Im Idealfall steht hier auch nur eine Zeile quellcode. Das lässt sich jedoch nicht immer vermeiden. Vor allem Dingen nicht, wenn komplexere Rückgabeparameter vorhanden sind.
public void TestGeneratingUsernameKentBrockmanShouldBeKenbro()
{
	// ARRANGE
	IPerson kent = new Person()
	{
		Forename = "Kent",
		Lastname = "Brockman"
	};
	IUserProcessor procesor = new StandardUserProcessor();
	string expected = "kenbro";
	
	// ACT
	string actually = procesor.GenerateUsername(kent);
	
	// ASSERT
	Assert.AreEqual(expected, actually);
}

Namenskonventionen

In meinem Artikel über Konventionen habe ich bereits geschrieben wie wichtig Konventionen überhaupt sind.

Da wir aber viele Tests schreiben, ist eine klare Regelung hier umso wichtiger.

Den nichts ist schlimmer als inkonsistenz.

Theoretisch sind zwar die Namen etc. nicht groß von Bedeutung, trotzdem sollte man ein einheitliches Muster erkennen können und dies auch verfolgen.

Ich gebe dir hier gleich zwei Muster an die Hand. Egal für welche du dich letztendlich entscheidest, sei konsistent und ziehe das Konzept verdammt noch mal durch.

Aus der Bezeichnung des Tests müssen mehrere Sachen hervorgehen:

  1. Dass es überhaupt ein Test ist (klingt zwar banal, wird aber häufig nicht gemacht).
  2. Der Name der Komponente, welche getestet wird.
  3. Der Klassenname, in der sich die Methode befindet.
  4. Die zu testende Methode selbst.
  5. Die Eingabeparameter der Methode.
  6. Das erwartete Resultat der Methode.

6 wichtige Parameter für einen guten Namen

Um das zu lösen, können wir wie folgt vorgehen:

  1. Wir erstellen ein neues Projekt und packen irgendwo „Test“ in den Namen.
  2. Pro Komponente, erstellen wir ein Namespace. Ob man das mithilfe von Unterordnern macht oder ein gewaltiges Test-Projekt, spielt in meinen Augen keine Rolle. Ein eigenständiges Projekt hätte nur den Vorteil, dass man die Projektmappe später aufteilen könnte.
  3. Für die Abbildung der Klassen haben wir genau zwei Möglichkeiten.
    1. Wir erstellen für jede Klasse ein Namespace.
    2. Wir erstellen für jede Klasse eine Testklasse.
  4. Abhängig vom der Wahl der Klasse, haben wir uns auch gleichzeitig festgelegt, wie wir die Methode abbilden können
    1. Pro Methode wird eine Testklasse erstellt.
    2. Pro Methode wird eine Testmethode erstellt.
  5. Unabhängig von der Wahl der Klassen und Methodenabbildung müssen die Eingabeparameter mit in die Definition der Methode geschrieben werden.
  6. Dies gilt ebenso für die Rückgabeparameter.

Dies ist meine Vorgehensweise um Unit-Tests zu erstellen. Das ist bestimmt nicht der alleinige Weg für die Namensgebung. In den letzten Jahren bin ich jedoch sehr gut damit gefahren.

Übrigens, die Sprache, in der der Name formuliert wurde, ist in diesem Fall egal. Es ist also vollkommen legitim die Unit-Tests auch in Deutsch zu schreiben.

In meinen Augen sind die Unit-Tests eine Art Dokumentation. Und eine Dokumentation kann schließlich ebenso in Deutscher Sprache formuliert werden.

Für was auch immer du dich entscheidest: Sei konsistent!

Fazit – Automatisierte Tests

Menschen Denken.

Maschinen Tun.

Und gerade weil wir denken, sind wir auch langsamer als Maschinen und können gewisse Dinge nicht mehrfach auf die identische Art und Weise machen. Wir können uns antrainieren Dinge ähnlich zu tun. Aber nie komplett identisch.

Monotones testen kann auch ganz schön nervig sein.

Durch Unit-Tests können wir diese Arbeit an die Maschinen abgeben und können so sicherstellen, dass bereits funktionierende Logik genauso funktioniert wie es auch funktionieren soll.

Natürlich dauert es Zeit einen Unit-Test zu schreiben. Natürlich muss man da zunächst einmal mitdenken und überlegen. Und natürlich kostet das im Endeffekt auch Zeit.

Letztendlich jedoch, zahlt sich das jedoch mehrfach wieder aus. Selbst noch während der Entwicklung.

Ein Projekt, welches viele sinnvolle Unit-Tests beherbergt und in dem viel Quellcode durch Tests abgedeckt wird (Code-Coverage), ist bei weitem nicht so anfällig für neue Fehlerquellen.

Was ist mit dir?

  • Schreibt zu bereits automatisierte Tests?
  • Wenn ja, welche Erfahrungen hast du bereits mit damit gemacht?
  • Schreibst du vielleicht andere Tests als „nur“ Unit-Tests? Beispielsweise Integrationstests?
  • Und wenn nicht, was hält dich davon ab?
Sei der erste und teile deine Meinung mit der Welt!
... und was meinst du dazu?
Deine E-Mail-Adresse wird nicht veröffentlicht.