Brauchen wir Tests und TDD? (Vortrag)

TDD Cycle

  1. Bevor du Code schreibst, schreibe einen Test, der das erwartete Verhalten deines Systems definiert. Der wird fehlschlagen weil der Production Code nicht da ist.

  2. Schreibe nur so viel Code, wie notwendig ist, um den Test zu bestehen. Schreibe keinen zusätzlichen Code.

  3. Refaktorisiere den Code, um die Lesbarkeit, Wartbarkeit und Effizienz zu verbessern, ohne dass sich das Verhalten des Moduls ändert.

Aber ist das sinnig? Wäre es nicht sinniger die Tests erst dann zu schreiben wenn du fertig bist?

Brauchen wir Tests überhaupt?

Auto

Das ist mein 2006 Civic. Als das Auto neu war, war es ein Billig-auto. Jetzt ist es noch billiger. Aber es hat dieses Coole Feature mit den Lichtern. Wenn du es startest dann gehen alle Lichter an… und wieder aus. Bleibt ein Licht brennen, dann weißt du Punktgenau wo du ein Problem hast.

Warum machen das Autos? Ist ja klar. Weil alle diese Systeme lebenswichtig sind und weil wir gerne jedes mal checken wollen ob sie noch laufen bevor wir ein Kraftfahrzeug fahren.

Software ist in Autos also lebenswichtig. Ist unsere Software lebenswichtig?

Was würde den passieren wenn kleinezeitung.at down wäre? Verluste. Jemand würde Geld fallieren. Wahrscheinlich nicht der Entwickler der für den Ausfall schuld war. Aber jemand würde so einen Ausfall bezahlen. Wie viele Leute aus der Regie würden wegen Budget-cuts dann gefeuert werden? Wie viele Leben können wir als Entwickler zerstören?

Das soll nicht bedeuten dass Tests dir die Superpower geben dass du alle Fehler im Voraus beheben kannst. Nicht ganz. Aber TDD ist da eine ziemlich große Hilfe. Ohne Tests, wie willst du wissen dass dein System funktioniert?

Beispiel aus dem Alltag

Ich lese dann etwas im Quellcode des Projekts an dem ich arbeite und stoße auf einen Controller. Der ist ein echter Salat. Mein erster Gedanke: “boa, das müssen wir aufräumen.” Mein nächster Gedanke: “Ich fass das ja nicht an! Weil wenn ich es anfasse, dann werde ich es kaputt machen und wenn ich es kaputt mache, dann werde ich für immer für diesen Controller verantwortlich sein”.

Ist das Kompetenz? Oder ängstliche Inkompetenz?

Wie würde angstlose Kompetenz aussehen?

Stell dir vor wir hätten solche schönen Lichter wie im Auto. Du änderst was am Source Code, du drückst einen Knopf und die Lichter gehen alle an… und alle wieder aus. Brennt ein Licht immer noch, dann weißt du Punktgenau wo das Problem ist. Bei uns hast du sogar eine Fehlermeldung in Text-Form die dir genau die Zeile zeigt wo das System zusammenbricht.

Du siehst einen Controller der Verbesserungsbedarf hat. Du verbesserst ihn. Drückst den Test-Knopf und die Tests sagen dir - GRÜN! Hast super gemacht.

Ja, dann extrahierst du eine Klasse aus dem Salat. ROT!

Ah ja, das ist das falsche Namespace, dann tust du die neue Klasse in das richtige Namespace. GRÜN! Und so weiter und so weiter. Das ist angstlose Kompetenz.

Der Refactor

… gibt uns die Möglichkeit Dinge zu verbessern. Und das erwarten unsere Kunden von uns. Weil wir als Menschen Dinge einfach besser machen. Wir Dekorieren das Haus, wir machen das Auto sauber, wir schmeißen den Müll raus. Was würde passieren wenn wir einen Monat den Müll nicht rauschmeißen würden? Das wäre einfach widerlich.

Was machen wir Entwickler aber? “Ja, ich weiss dass das nicht so schön ist, aber ich muss es vor Dienstag fertig haben und wenn es fertig ist, dann werde ich es aufräumen.”

Später haben wir dann keine Zeit. Es kommt ein neues Feature und wir müssen das dann schnell fertig machen bevor wir aufräumen.

Ein Jahr später - “ich fass das ja nicht an!”

Und so wird unsere Software immer schlimmer und schlimmer. Der Müll bleibt im Haus. Und wir tun so als ob wir ihn nicht riechen.

Das muss nicht so sein. Wie komplex sind aber Tests?

Beispiel

Weather Command Factory Test Example

Ganz einfach, oder?

Kritiker sagen dass Unit Tests zu fragil sind. Sie sagen:

“Wenn ich einen Refactor mache, dann muss ich auch den Test ändern”

Musst du nicht. Das musst du nur machen wenn du die Tests an den Production Code koppelst. Und das ist in den meisten Fällen nicht notwendig.

Btw, bei der Kleinen Zeitung machen wir das so:

KLZ Test Befehle im Shell

Unit Tests oder End to end?

Das was ich euch oben gezeigt habe ist ein Unit test. Was ist ein Unit? Keine Ahnung aber es ist klein.

Was ist ein e2e Test?

Ein End to End Test

Du gehst durch den ganzen Stack, inklusive Framework und Datenbank. Machst du bei Unit Tests nicht weil die Unit Tests klein und Isoliert sind.

Es kann aber passieren dass alle deine Unit tests grün sind und die App trotzdem nicht läuft. Z.B. könnte es wie bei Facebook passieren, dass jemand einen Router falsch einstellt. Die App ist dann Fehlerfrei aber in Production ist sie unbrauchbar. Deswegen sind e2e Tests sehr sinnig.

Aber oft wird diese Pyramide angezeigt:

Test Pyramide

Ein paar, wenige e2e Tests und ein gigantischer Haufen Unit tests.

Warum? Wo ist der unterschied zwischen e2e und Unit Tests?

Wir nennen 2 Unterschiede.

1. Zeit

Unsere Unit Test Resultate

Unsere End to End Test Resultate

Zeit für EINEN Test:

  • Unit: 0.21s
  • End to end: 1.83s

Also hätten wir 185 e2e Tests, würden wir fast 6 Minuten warten bis die fertig sind. Stell dir vor du willst dann eine Funktion umbenennen oder schauen ob das löschen von einer Zeile Code das System kaputt macht. Du willst für sowas keine 5 Minuten warten.

Also Unit tests sind enorm viel schneller als e2e Tests.

2. Koppeln

Mit e2e Tests machst du ein paar annahmen. Du nimmst an dass du einen Webserver hast, vielleicht eine Domain. Wenn du die DB checkst, dann nimmst du an dass du eine Datenbank hast und dann brauchst du evtl. auch eine Verbindung zu dieser Datenbank. Das koppelt deine Tests an unwichtige Details.

Wie unwichtig? Der Webserver, die Datenbank, sogar das Framework sind Implementations-Details. Wieder das Beispiel unserer Wetterseite -> das laden von Wetterdaten ist bei uns komplett Framework-agnostic.

require klz/weather im Shell

Wetter-Paket-Nutzung

Output einer Weather Command

Stell dir vor dass das Wettermodul nur mit e2e Tests geschrieben wäre.

Noch mal ein End to End Test

Klein und einfach. Aber woher kommt die $this->json() Methode? Aus dem Framework.

Hätten wir das Wettermodul nur mit e2e Tests gebaut, dann könnten wir es nicht aus der API rausnehmen. Die Wetter-Logik wäre an die Controller, an die Ruten und die sonstigen Framework Sachen gebunden. Das Problem passiert aber oft, oder?

Wäre es aber nicht cool wenn wir dem ausweichen könnten? Deine App muss nicht an das Framework gebunden sein. Das Framework ist ein Plugin zu deiner App. Genauso der Webserver, die Datenbank und jegliche anderen Servise.

Stell dir vor, die Wetterseite nutzt eine 10 000€ pro Monat Oracle DB und du kannst auf MySQL migrieren. In Minuten.

Was würde das für die Kleine Zeitung bedeuten? Wir hätten vielleicht jemanden in der Regie behalten können.

Stell dir vor eine neue Laravel Version kommt und du musst das Update nicht vornehmen. Es geht automatisch weil deine App sowieso nicht direkt an das Framework gebunden ist.

Was würde das für uns bedeuten?

Immer aktuelle Dependencies zu haben… Umsonst! Das geht. Ist aber ein Thema für ein anderes Meeting.

Fazit Unterschiede

Noch mal das Bild:

Test Pyramide

Also wir schreiben enorm viele Unit Tests weil sie super schnell sind und weil sie uns beim Design helfen. TDD ist nicht nur eine Testing-Disziplin. TDD hilft dir zu einem besseren Design zu kommen.

Besser als was? Mit TDD bekommst du ein besseres Design als wenn du die Tests nach dem Production Code geschrieben hättest.


Wichtig: wenn ich sage dass wir eine gigantische Anzahl an Unit Tests schreiben, meine ich nicht dass wir redundante Tests schreiben.

Wir brauchen nicht 100 Tests die zu erst durch den Login gehen damit sie zu dem Teil der App kommen den sie Testen.

Lernen

  • Ich will Tests schreiben die kein Aufwand sind.
  • Ich will Tests schreiben die nicht an den Code den sie testen gekoppelt sind,
  • Ich will Tests schreiben die extrem schnell sind.

Wie geht das?

Alle Beispiele sind in Java. Das ist für uns natürlich kein Problem weil die Sprache nur eine Sprache ist und die Prinzipien hier weitaus über der Sprache gehen.

Wenn euch aber konkrete Tools interessieren für die Kleine Zeitung. Wir benutzen

Es gibt in der php community ein neues Testing framework, es nennt sich Pest. Sollte Cool sein, vielleicht steigen wir darauf um, ist einen Blick wert.

Schaut euch die Sachen mal an. Insbesondere Clean Code. Der Typ ist etwas org aber der Inhalt der Kurse ist Gold wert.

Mich würde eure Meinung zu den Kursen interessieren.


TDD lernt man. Menschen (wie ich) machen dabei Fehler. Du bekommst durch TDD nicht einfach ein Perfektes System.

Es geht aber um Disziplin. TDD ist wie Zähneputzen. Stell dir vor du Putzt deine Zähne mit Seife weil dir noch keiner Zahnpasta vorgestellt hat. Das ist nicht angenehm. Eines Tages wirst du lernen deine Zähne mit Zahnpasta zu putzen und das wird zur deren Gesundheit beitragen.

Stell dir vor dein freund sagt dir aber dass es nicht wert ist deine Zähne mit Seife zu putzen weil es einfach zu anstrengend ist. Er zeigt dir nicht wie man Zähne mit Zahnpasta putzt sondern sagt dir nur dass es den Aufwand nicht wert ist. Er meint: Du kannst auch hin und wieder zum Zahnarzt gehen. Eines Tages bekommst du eh eine Prothese also ist es egal.

Nein. Auch wenn du nur Seife nutzten tust, ist es immer noch besser deine Zähne so diszipliniert wie möglich zu putzen, statt beim Zahnarzt später Schadensbegrenzung durchzuführen. So ist es auch mit TDD. Was ist die alternative zu TDD? Schadensbegrenzung. Debugging. Das muss nicht so sein. Gibt der Disziplin eine Chance.

Heute bist du vielleicht nicht so flott mit TDD. Du lernst es noch. Es sieht vielleicht sogar schwierig aus. Aber halt durch! Höre nicht auf zu lernen. Schon bald wird es zur einer guten Gewohnheit. Wie das regelmäßige Zähneputzen. Und es wird nicht so schwierig sein wenn du weißt wie es gemacht wird. Wenn du lernst dass man Zähne mit Zahnpasta putzt.

Fazit

  • Tests zu schreiben ist nicht teuer. Jedes Billig-auto hat welche
  • Der Grund warum wir Tests schreiben und TDD praktizieren ist die Erstellung einer sicheren Umgebung wo wir problemlos refaktorisieren können
  • Unit Tests fördern gutes Design. Design welches unabhängig von Details ist (wie der Datenbank, dem Framework usw.)
  • Lernen braucht Zeit, es ist aber den Aufwand wert