Über unsMediaKontaktImpressum
Alex Schladebeck 01. Juni 2021

Unit Testing und testgetriebene Entwicklung für Tester – ein Erfahrungsbericht

Wenn ich gefragt werde, welche Einstellung am wichtigsten bei Testern ist, antworte ich meistens mit: "man akzeptiert, dass man immer wieder dazulernen muss". Nichtsdestotrotz gab es lange Zeit einen Aspekt der Qualitätssicherung, über den ich behauptet habe, dass Tester damit eher nichts zu tun haben. Das war für mich Unit Testing und Test-Driven Development. Während ein Unit Test ein Verhalten in Isolation testet, werden beim Test-Driven Development die Tests vor dem Code geschrieben.

Ich hatte es ganz lange als alleinige Verantwortung der Entwickler im Team abgeschrieben. In den letzten Jahren hat sich meine Meinung dazu allerdings geändert. In diesem Artikel erkläre ich, weshalb und was ich für mich als Tester und als Befürworter von ganzheitlicher Qualität dazu gelernt habe.

Ausgangslage: was ich meinte, zu wissen

Wie oben erwähnt, Unit Testing und Test-Driven Development sind mir schon lange gängige Begriffe. Irgendwo am Anfang meiner Reise mit Qualitätssicherung habe ich gelernt und verinnerlicht, dass der Entwickler Unit Tests zu seinem Code schreibt. Entweder vor dem Implementieren oder kurz danach. Es macht nicht mal Sinn, dass ein anderer Entwickler die Unit Tests für den Code eines anderen Entwicklers schreibt, so eng sind die beiden Aktivitäten miteinander verbunden. Dazu hatte ich dank einfacher Beispiele das Gefühl, dass Unit Testing sehr einfach sowohl von der Durchführung als auch von der Disziplin ist. Im Vergleich zu großen Workflows auf der Benutzeroberfläche müsste es doch relativ einfach sein, einzelne Verhalten vom Code zu testen…

Was hat sich geändert?

Trotz dieser Glaubenssätze hat mich das Thema doch interessiert. In Gesprächen mit Entwicklern schien es doch nicht so einfach zu sein. Unit Tests in einem System nachträglich hinzuzufügen ist sehr schwer, habe ich gelernt. Es gibt genügend Projekte, in denen man mit den Entscheidungen und dem Design von vor vielen Jahren umgehen muss. Außerdem ist die Welt viel komplizierter, als die einfachen Beispiele, die man im Internet finden kann. Und ich hatte unterschätzt, wie schwer es sein kann, sein eigenes Verhalten zu verändern. Wenn man es gewohnt ist, nicht an Unit Tests oder testbaren Code zu denken (durch das Studium, Praktika, eigene Projekte, Online-Kurse und dann schließlich genau solche schwer testbaren Legacy-Projekte), dann ist das Umdenken und die dazugehörige Disziplin ein großer und aufwändiger Schritt. Dieses Umdenken verlangsamt und unterbricht den Fluss an Implementierung – und das im normalen Projektgeschäft, wo Zeit immer ein Faktor ist.

Diese Einsichten und etwas Empathie habe ich in Gesprächen mit Entwicklern dazu gelernt. Als Tester in Projekten und als Verantwortliche für Qualität im Unternehmen sah ich hier durchaus Potential für Tester und Entwickler. Auf einer Testkonferenz habe ich mich dann getraut – ich besuchte einen Workshop für Unit Testing aus der Testerperspektive. Ich habe sehr schnell drei Sachen bemerkt:

  1. Das systematische Denken und die analytischen Fähigkeiten, die ich als Tester schon besaß, waren auch auf dieser Ebene nützlich. Wissen über Äquivalenzklassen [1], wiederkehrende Fehler und Systemdesign führten mich zu den "richtigen Fragen".  
  2. Vieles, was häufig noch als Integrations- oder UI-Test geschrieben wird, könnte schneller und effektiver als Unit Test geschrieben werden.
  3. Ich konnte mir vorstellen, wie hilfreich es wäre, beim Testen einer Funktion zu wissen, welche Unit Tests existieren.

Nach dem Workshop habe ich weitere Gespräche mit Entwicklern geführt. Ich habe durch Pairing und Ensemble Programming weiter Bestätigung gefunden, dass meine Fragen für Unit Tests hilfreich sind [2]. Ich habe nach weiteren Beispielen (im Internet, auf Konferenzen und in Gesprächen) gesucht und habe bemerkt, dass manche Unit Tests nur wenige Äquivalenzklassen abbildeten. Oder ungenügende (gar fehlende!) Asserts hatten.

Es wäre doch ungerecht, wenn der Tester sich nicht mit Unit Testing auseinandersetzt.

Langsam festigte sich eine neue Meinung. Wenn ich an Whole Team Quality glaube – also, dass jeder im Team Mitverantwortung an der Qualität trägt (auch wenn ein Tester im Team eingebettet ist) – dann kann ich doch kaum einen Bereich ignorieren. Andersherum gesagt – ich habe die Erwartung, dass sich Entwickler API-Testing und UI-Testing zum Teil aneignen, um unterstützen zu können. Da wäre es auf der einen Seite ungerecht und auf der anderen Seite ineffizient, wenn der Tester sich nicht mit Unit Testing ebenfalls auseinandersetzt, um auch im Sinne von Whole Team Quality unterstützen zu können und geteiltes Verständnis zu erreichen.

Die andere alte Meinung – "das ist doch einfach, weil es kleine Tests sind" – wandelte sich auch. Ja, die einzelnen Tests sind klein. Aber sich im Detail zu überlegen, wie genau etwas funktioniert (also in viel mehr Detail als je in einer Anforderung oder User Story steht) – und sich zu fragen, wie man dieses Verhalten (und nur dieses Verhalten) abtesten kann – ist doch recht kompliziert. Vor allem wenn, aus nachvollziehbaren Gründen, man es noch nicht gewohnt ist.

Ich sah für mich persönlich, für unsere Test Community und für die Entwickler und Projekte eine Gelegenheit, mehr zu lernen und unsere eigene Qualitätspraktiken zu verbessern.

Das Beispiel, bei dem es Klick machte...

Bevor ich das Thema mehr in die Firma tragen konnte, wollte ich es selbst noch besser verstehen. Mir war klar, dass ich eventuell Widerstand von "beiden Seiten" bekommen würde: ich bin selbst kein Entwickler und kann schwer nachvollziehen, was genau diese brauchen. Für manche Tester könnte es auch schwer sein, sich vorzustellen, dass man über "sehr technische" Details redet. Mein Ansatz: Je mehr ich selbst darüber erfahre, desto höher sind meine Chancen, andere Kollegen zu überzeugen.

Wie oben erwähnt, sind einfache Beispiele im Internet überall zu finden. Das untenstehende Beispiel ist auch nicht das komplizierteste – allerdings war es für mich einsichtiger als ein Taschenrechner (ein klassisches Beispiel für Unit Testing). Ich habe das Beispiel in einer Pairing-Sitzung mit einem Entwickler durchgearbeitet und nutze es mittlerweile in Workshops für Tester zu diesem Thema. Wir haben es damals Test-First (mittels TDD) entwickelt.

Die zu schreibende Methode

Für eine Inhouse-Applikation wollten wir eine Methode schreiben, um die Durchwahl einer kompletten Telefonnummer zu extrahieren. Die komplette Telefonnummer hat das Format +495311234527, wobei +4953112345 das wiederkehrende Muster darstellt und die darauffolgenden Zahlen die Durchwahl.

Die Tests

Wir haben eine Testliste gemacht und sie in gültige und ungültige Inputs unterteilt. Die Liste unten ist vereinfacht, um nur auf die wesentlichen Details einzugehen. Es sind durchaus mehr Tests nützlich und notwendig!

InputErwarteter OutputKommentar
+495311234566Einstellige Durchwahl
+49531123452727Zweistellige Durchwahl
+491234567279279Dreistellige Durchwahl
+49531123452779Empty String
Log Output: ungültige Telefonnummer übergeben.

Aktuell haben wir keine vierstelligen Durchwahlen. Somit ist das aktuelle Verhalten dokumentiert. Statt eine Fehlermeldung zu sehen, sieht der Nutzer einfach nichts in der Oberfläche.

+4953112345

Empty String
Log Output: ungültige Telefonnummer übergeben.

Wenn keine Durchwahl vorhanden ist, zeige einfach nichts in der Oberfläche.
Zudem soll die Methode loggen (für Admins oder Ops), dass eine ungültige Telefonnummer als Input übergeben wurde.

Null, empty stringEmpty String
Log Output
Wie oben
abcdeEmpty String
Log Output
Wie oben

Ungültige Muster, z. B.
+4953112345/21
+49531 12345 21
+4433377722

Empty String
Log Output
Wie oben

Wir sind schrittweise durch die Tests iteriert und haben nach dem Paradigma Red-Green-Refactor gearbeitet. Zunächst schreibt man einen Test, der fehlschlagen wird (rot). Danach schreibt man den minimalen Code, um diesen Test grün zu bekommen (aber nur diesen einen Test!). Danach verbessert man die Implementierung (bessere Namen, andere Strukturen), ohne das Verhalten zu ändern (refactor).

Der allererste Test könnte sein:

@Test
public void twoDigitTelephoneNumber() {
    assertEquals("27",TelephoneNumberUtil.extractExtension("+495311234527"));
}

Der Code, der genau diesen Test grün macht, könnte so aussehen:

public static String extractExtension(String telephoneNumber) {
    return "27";
}

Natürlich wird dieses Verhalten nicht lange bestehen. Die "27" hartcodiert zurückzuliefern wird nicht lange richtig sein. Aber erst der nächste Test wird uns dazu bringen (treiben), die Implementierung anzupassen.

Der dritte Schritt – refactor – ist wichtig, damit man Test-Code und Implementierung besser strukturieren und benennen kann ohne den Test wieder rot zu machen. In diesem Fall haben wir die Testmethode umbenannt:

@Test
public void telephoneNumberWithTwoDigitExtensionReturnsTwoDigitExtension() {
    assertEquals("27", TelephoneNumberUtil.extractExtension("+495311234527"));
}

Natürlich sind das nur die ersten drei Schritte. Aber durch diese Erfahrung hat auf einmal vieles "Klick" gemacht.

Was machte denn alles "Klick"?

  • Test-Driven
    Der erste Aha-Moment war, als wir das erwartete Verhalten für die negativen Tests definieren wollten. Mein erster Instinkt war es, im Code nachzuschauen, was passieren soll. Als es mir dämmerte, dass ich das gerade selbst definieren soll, hatte ich einen richtigen "Aha-Moment".
  • Disziplin
    Ich habe ebenfalls bemerkt, wie viel Disziplin man braucht, um immer nur den nächsten kleinen Schritt zu machen.
  • Naming is hard
    Ich kannte seit Jahren Witze darüber, dass Benennung eines der größten Probleme in der Programmierung sei – aber erst bei der vermehrten Umbenennung von Funktionen, Tests und Variablen um ihre genaue Bedeutung zu erklären, habe ich verstanden, wie wichtig und wie schwer Namen sind.
  • Bei Test-Driven Development geht es um das Design
    In diesem Beispiel haben wir explizit Test-Driven Development eingesetzt. Mein Verständnis von dem Begriff ist seitdem: "Durch Test-Driven Development erkunden wir schrittweise das notwendige Design für die Methode. Als Bonus erhält man Artefakte (Unit Tests), die das Verhalten auch bei Änderungen überprüfen und zudem auch dokumentieren". Ja, man arbeitet mit Tests, aber es geht zunächst um das Design: "Wie muss diese Methode geschrieben werden, um genau das Verhalten zu leisten und nicht mehr?".
    Natürlich kann man Unit Tests auch im Nachhinein schreiben. Wenn man die Tests nachträglich schreibt, macht es nach meiner bisherigen Erfahrung Sinn, die Tests direkt im Anschluss zu schreiben – und auch kleinschrittig. Ansonsten passt das Design eventuell nicht und man muss erst Änderungen an der Implementierung machen, bevor man es testen kann. Meine Präferenz liegt trotzdem beim Test-First.
  • Nicht alle Tests müssen behalten werden
    Bei diesem Beispiel haben wir alle Tests behalten, die während des Test-Driven-Prozesses entstanden sind, aber ich habe seitdem andere Beispiele erlebt, bei denen manche Tests nur beim Entwickeln notwendig sind. Sie treiben wortwörtlich den nächsten kleinen Schritt, aber später sind sie redundant aus der Betrachtung von Äquivalenzklassen/Grenzwertanalyse und können gelöscht werden.

  • Detaillierte Dokumentation
    Die Beispiele in der Tabelle oben sind bei weitem nicht alle, die man für diesen Fall testen könnte. Aber durch den Fokus auf Testen haben wir das gewünschte Verhalten (sowohl für gültige als auch ungültige Eingaben) nicht nur abgesichert, sondern auch dokumentiert. Der nächste Entwickler kann sehr einfach nachvollziehen, was genau wie funktionieren soll (und welche Verhalten nicht funktionieren sollen).

  • Änderungsindikatoren
    Schon bei der ersten Sitzung haben wir bemerkt, dass die Tests uns vor ungewollten Änderungen schützen. Bei einem gedachten Refactoring-Schritt schlug plötzlich ein Test fehl – irgendwo hatten wir einen Fehler gemacht und konnten ihn schnell identifizieren, anpassen und wieder überprüfen.

  • Nicht alle Tests sind "möglich"
    Ich war es eher gewohnt, über die UI zu testen. Das bedeutet, ich versuchte durchaus z. B. Strings in Zahlenfelder zu tippen. Man kann aber in Java keinen Unit Test für eine Methode schreiben mit int als Eingabeparameter und versuchen, einen String einzugeben. Nicht alle Testideen, die ich hatte, konnte ich also verwenden.

Nach dem Erfolg ist vor dem Erfolg

Nach diesem Beispiel war ich noch begeisterter, mit Entwicklern und Testern über Unit Testing und Test-Driven Development zu reden. Durch mein bisheriges Wissen und Interesse konnte ich weitere Entwickler finden, die bereit waren, mit mir zu üben (FizzBuzz war das nächste Beispiel, das wir uns angeschaut haben [3]). In dieser Zusammenarbeit haben wir beschlossen, unsere eigenen Entwickler durch Trainings und Workshops mehr auszubilden und sichere Übungsformate innerhalb der Firma anzubieten. So entstand ein Quality Dojo, in dem wöchentlich Entwickler und Tester zusammenkommen, um Beispiele in Test-Driven Development außerhalb des Projektkontexts zu üben. Je mehr Leute sich damit auseinandersetzten, desto mehr hat das Thema durch Cross-Projekt-Beratung und -Unterstützung gegriffen. Das Quality Dojo ist auch für Tester offen – und durch die Erfahrung haben wir Workshops "Unit Testing für Tester" entwickelt und merken, dass Tester immer mehr beim Schreiben von Unit Tests unterstützen. Entweder durch Pairing, durch Gespräche und Vergleiche der Tests oder auch durch das Erweitern von schon geschriebenen Tests. Ich versuche weiterhin, an Pairings und Ensemble-Sitzungen teilzunehmen, um selbst weiter zu üben.

Never say never…

Mittlerweile vertrete ich eine ganz andere Meinung zum Thema Unit Testing und Test-Driven Development für Tester. Als Tester ist es hilfreich zu wissen, wie der Code implementiert ist. Es ist unheimlich wichtig zu wissen, was schon getestet ist – und sogar diese Tests erweitern und verbessern zu können. Wie bei allen anderen Entwicklungsaktivtäten, ist ein Vier-Augen-Prinzip bei Unit Tests nützlich – und das andere Paar Augen darf durchaus von einem Tester kommen. Ich kann nur dazu ermutigen, sich als Tester damit auseinanderzusetzen, auch wenn man keinen Entwicklungshintergrund hat. Und an alle Entwickler: seid offen, wenn die Tester sich dazugesellen wollen. Es bringt für alle Vorteile!

Autorin

Alex Schladebeck

Alex Schladebeck ist Testerin aus Leidenschaft und Beraterin für Qualität und Agilität. Sie leitet den Bereich Qualitätssicherung und ist Geschäftsführerin bei der BREDEX GmbH.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben