Bevor Sie weiterlesen: Für Softwaretests hinsichtlich Anforderungen gilt oftmals das Gleiche wie für Besuche beim Arzt. Eine Diagnose stellt fest, dass es keine Beweise für eine Krankheit gibt. Beweise für die Abwesenheit einer Krankheit gibt es nicht. Sie werden es schwer haben, in einem alltäglichen Projekt, die Abwesenheit von Fehlern hinsichtlich Anforderungen zu beweisen.
Den vollständigen Quelltext dieses Artikels finden Sie auf GitHub.
Vertrauensbildende Maßnahmen: Tests und Dokumentation
Tests und Dokumentation sind wichtige Aspekte einzelner Anwendungen und der meisten Anwendungssysteme. Beide Themen sind vielschichtig und finden auf unterschiedlichen Ebenen statt, zum Beispiel auf Methoden-, Klassen-, Modul- und Systemebene. Tests werden genutzt, um zu überprüfen, dass einzelne Komponenten ihre Spezifikation erfüllen. Das sind in der Regel Unittests. Der nächste Schritt ist sicherzustellen, dass Komponenten miteinander funktionieren. Es wird von Integrationstests gesprochen. Regressionstests können sowohl als Unit- als auch Integrationstest ausgeprägt sein. Regressionstests sollen Fehler nach Änderungen von Komponenten aufdecken. Regressionstests müssen also wiederholbar sein, um das Ergebnis eines alten mit dem Ergebnis eines neuen Testfalls vergleichen zu können.
Hinsichtlich Dokumentation ist das Feld ähnlich vielfältig. Es wird von Code-, API-, Architektur- und Anwenderdokumentation gesprochen.
Trotz aller Unterschiede gibt es eine Gemeinsamkeit: Die genannten Maßnahmen schaffen Vertrauen. Vertrauen in die Funktionalität als solche, in die Integrierbarkeit eines Systems und auch darauf, Änderungen vornehmen zu können.
Warum sparen wir uns dennoch das Testen?
Konsequente Softwaretests — so sie denn gesetzlich nicht vorgeschrieben sind — stehen oftmals hinten an oder sind nicht integraler Bestandteil von Softwareentwicklung. Im Projektalltag werden oft Varianten folgender Argumente vorgebracht: “Dafür ist keine Zeit da.”, “Testen schafft sowieso keinen Mehrwert.”, “Diese Module sind nicht testbar.” oder auch “Das benutzte Framework macht Tests zu aufwendig.”
Demgegenüber sei entgegnet:
- Die Zeit wird in Summe so oder so aufgewendet. Vielleicht nicht durch dasselbe Team, das einen Service erstellt hat, aber dann durch das Wartungsteam oder den Support. Die nachträgliche Fehlersuche und insbesondere das dann hoffentlich durchgeführte Testen sind teurer.
- Testen schafft Vertrauen. Vertrauen, das Refactorings und neue Features unterstützt und damit direkt Mehrwert entspricht.
- Testen stellt sicher, dass nicht geänderte Programmbestandteile nach Refactorings weiter funktionieren.
- Code, von dem bekannt ist, dass er getestet wird, wird von Anfang an anders und in meinen Augen besser strukturiert, so dass er testbar bleibt.
Schleicht sich in einem Projekt Stress ein, sei es durch zeitlichen Druck, unklare Anforderungen oder anderes mehr, ist es zu spät, Testen noch in den Fokus zu rücken. Hinterher Tests zu schreiben bringt oftmals keinen direkten Mehrwert mehr und eine Nachdokumentation macht selten Spaß.
Anforderungen an Werkzeuge und Tests
Es ergeben sich aus den einleitenden Abschnitten mindestens die folgenden Anforderungen an Tests:
- Der Start eines Projektes darf mit Testunterstützung nicht aufwendiger sein als ohne.
- Die Tests müssen sich nahtlos in den Entwicklungsprozess integrieren.
- Sie müssen so schnell wie möglich ausführbar sein.
- Das Ergebnis sollte meßbar sein.
Die Teams hinter Spring und Spring Boot legen großen Wert darauf, dass ihre Frameworks einen testgetriebenen Softwareenwicklungsansatz unterstützen.
In der Java-Welt hat sich JUnit als Standardwerkzeug zur Ausführung von Tests durchgesetzt. JUnit wird von Spring — auch in der aktuellsten Version 5 — vollumfänglich unterstützt.
Das Spring-Framework wurde 2002 erstmals als Idee vorgestellt und ein Jahr später unter dem Namen Spring-Framework als quelloffenes Projekt veröffentlicht. Das Ziel — damals wie heute — ist, die Entwicklung mit Java zu vereinfachen und gute Programmierpraktiken zu fördern.
Kernfunktionen von Spring sind:
Dependency Injection
MVC-basierte Webanwendungen und RESTful Webservices
Grundlagen für JDBC, JPA und vieles mehr
aspektorientierte Programmierung und deklarative Behandlung von Transaktionen
Spring Boot ist in diesem Kontext kein neues Framework, sondern eine umfassende Lösung, die Abhängigkeitsmanagement, Build und vieles andere erheblich vereinfacht. Neue Spring-Projekte sollten daher mit Spring Boot umgesetzt werden.
Spring-Boot-Anwendungen sind ganz normale Java-Anwendungen, die in der Regel mit einem Build Management Tool gebaut werden. Das Build Management Tool ist unter anderem für die Auflistung und Bereitstellung aller Abhängigkeiten zuständig. Im Beispielprojekt zum Artikel wird das Werkzeug Gradle verwendet.
Spring Boot arbeitet mit sogenannten Startern. Diese Starter stellen Ihnen alle für ein gegebenes Thema notwendigen Abhängigkeiten zur Verfügung. So einen Starter gibt es auch für das Thema “Testen”.
Die Deklaration der Abhängigkeit in einem Gradle Build File
(build.gradle
) ist sehr einfach, wie
Listing 1 zeigt.
Durch nur eine Deklaration erhalten Sie:
- JUnit
- Springs Test-Support
- Mockito
- AssertJ, eine Library, die es Ihnen ermöglicht, sehr klare und ausdrucksstarke Zusicherungen (Assertions) zu erwarteten Ergebnissen zu formulieren
Das Beispiel
Ich möchte Ihnen anhand einer einfachen Fachlichkeit zeigen, wie Spring Boot 2 Ihnen dabei hilft, sehr einfach Integrationstests zu schreiben: Sei es als vollständiger Durchstich oder als Integrationstest auf einer technischen Ebene.
Getestet werden soll ein Service, der Events und dazugehörige Registrierungen verwaltet. An einem Tag können mehrere Events stattfinden, die Namen der Events müssen eindeutig sein. Events haben eine begrenzte Teilnehmeranzahl. Interessierte Besucher melden sich mit Namen und E-Mail-Adresse an und sollen sich nicht mehrfach anmelden können.
Der Event-Service könnte Teil einer größeren Anwendung sein und als sogenannter Bounded Context identifiziert worden sein. Der Begriff Bounded Context stammt aus dem Domain-driven Design (DDD). Ein Bounded Context zielt darauf ab, größere Modelle in kleinere Teile zu zerlegen, die für sich genommen handhabbar sind und definierte Beziehungen untereinander haben. Innerhalb eines Bounded Context wird mit einer gemeinsamen, allgegenwärtigen (“Ubiquitous Language”) über ein Thema gesprochen. Diese Fokussierung ist nicht nur für die eigentliche Entwicklung, sondern auch für das Testen wichtig. Es wird klar erkennbar was Teil des Tests sein muss und was nicht. Sie vermeiden damit, in einem allumfassenden Kontext ein ebenso allumfassendes Modell der Welt testen zu müssen.
Die Fachlichkeit eignet sich sehr gut zu zeigen, dass Tests auf
Modulebene nicht nur sehr einfach zu realisieren, sondern auch oftmals
die wichtigsten Aspekte Ihrer Domain bereits erfassen. Betrachten Sie
die Klasse Event
in Listing 2.
- Der Konstruktor überprüft alle geforderten Vorbedingungen. Client-Code kann kein ungültiges Event herstellen.
- Die Registrierung selber: Es ist nicht notwendig, Logik dieser Art über einen Service zu implementieren und das Event auf ein blutleeres Modell (anemic domain model) zu reduzieren.
Die Klasse Event
wird in einem Domain-driven Design Ansatz als
Aggregate Root bezeichnet, als Kern Ihrer Domain. Ein Aggregat kapselt
mehrere Objekte Ihrer Domain, auf die nur gemeinsam zugegriffen werden
darf. Eines dieser Objekte ist das Root-Objekt. Im Beispiel ist Event
das Root-Objekt, die Registrierungen sind Objekte, die nur im Kontext
des Events Gültigkeit besitzen. Auch hier hilft das gezielte Abstecken
des Rahmens bei der Festlegung dessen, was getestet werden soll.
Event
ist frei von Spring-typischen Annotationen. Schauen Sie in den
vollständigen Quelltext finden Sie allerdings JPA-Annotationen. JPA
steht für Java Persistence API und wird genutzt, um die Inhalte
relationaler Datenbanken auf Objekte abzubilden. Richtig genutzt
verbinden Sie damit sinnvolle Datenbankschemata mit Objekten, die nach
den im vorherigen Absatz beschriebenen Prinzipien gestaltet wurden.
Die Tests
Auf Modul-(Unit)-Ebene
Durch die Abhängigkeit spring-boot-starter-test
erhalten Sie alle
Bausteine, um Event
einem Unit-Test zu unterziehen.
Listing 3 zeigt Tests der erwarteten Pre- und Postconditions.
- Signalisiert, dass diese Methode von JUnit als Testmethode ausgeführt werden soll
- Hier sehen Sie eine AssertJ-Assertion. AssertJ bietet unter anderem eine schöne Möglichkeit an, zu testen, ob eine bestimmte Exception geworfen wurde oder nicht.
- Weiterhin ist es möglich, Assertions aufeinander aufzubauen: Wenn die Exception dem erwarteten Typen entsprach, wird zusätzlich die entsprechende Mitteilung überprüft.
Der Test der Logik ist ähnlich aufgebaut. Der Test ist klar strukturiert
und gut lesbar, insbesondere weil die zu testende Klasse die Domain gut
widerspiegelt. Der Kern des Event-Services kann so mit wenig Aufwand
fast vollständig getestet werden. In einem vollständig testgetriebenen
Ansatz könnten Sie soweit gehen, dass Sie zuerst den Unit-Test wie in
Listing 3 gezeigt schreiben und dadurch
Ihre Erwartungen an die Domain formulieren. Da der Test so natürlich
nicht kompiliert, müssten Sie anschließend die zu testende Klasse
Event
anlegen, die notwendigen Schnittstellen definieren und
implementieren, bis der Test kompiliert und schlussendlich von rot
(schlägt fehl) auf grün umspringt.
JUnit 5 erschien im Herbst 2017. Es hat viele neue Funktionen, unter
anderem die Möglichkeit, Testklassen als @Nested
zu markieren. Das
vermeidet zum Beispiel das komplexe Setup statischer Klassen in
Listing 3. Darüber hinaus JUnit 5
vereinfacht den Umgang mit parametrisierten Tests und das Testen von
Exceptions. JUnit 5 kann dynamische Tests (das sind Tests, die erst zur
Laufzeit generiert werden) ausführen und mit @DisplayName
sinnvolle
Namen vergeben. Default-Methoden in Interfaces können als @Test
annotiert werden und so als Mix-In in andere Tests gebracht werden.
Die aktuelle Version des Spring-Frameworks und Spring Boot unterstützen ebenfalls JUnit 5. Durch Ausschluss der JUnit-4-Abhängigkeit des eingangs erwähnten Starters und Deklaration der entsprechenden JUnit-5-Bibliotheken hätten Sie alles, was Sie zum Schreiben von Tests benötigen. Warum also nicht direkt in diesem Artikel JUnit 5 nutzen?
Zur Infrastruktur eines Testes gehört mehr als nur die Abhängigkeit, auch die restliche Kette des Buildprozess muss einfach bereitgestellt werden können. Bürden Sie Ihren Entwicklern zu viel Aufwand zum Testen auf, wird nicht getestet. Während die Unterstützung von JUnit 5 in den großen IDEs wie Eclipse und IntelliJ IDEA bereits sehr gut ist, funktioniert die Build-Tool-Unterstützung noch nicht wie erwartet. Das Beispielprojekt zum Artikel arbeitete ursprünglich mit JUnit 5 und ich hätte Stand November 2017 einen eigenen Aufsatz darüber schreiben können, wie man Gradle beziehungsweise Maven dazu bringt, ähnlich gut mit JUnit 5 zusammenzuarbeiten wie mit Version 4. Ich habe mich bewusst dagegen entschieden, um tatsächlich den Fokus auf einfaches Testen zu legen.
Es ist davon auszugehen, dass die Integration von JUnit 5 in den Build-Werkzeugen im Laufe von 2018 ähnlich gut sein wird, wie sie jetzt für JUnit 4 ist.
Fließende Grenzen
Die Events kommen aber nicht aus dem luftleeren Raum. Sie werden in einer Datenbank gespeichert und es muss eine Schnittstelle geben, sie abzurufen. Eine gute Möglichkeit für Datenbankzugriffe vielfältiger Art ist eines der vielen Spring-Data-Module. Spring Data implementiert für Domain-Klassen das Repository Pattern. Ein Repository dient als Schnittstelle zwischen der Domainschicht und dem technischen Zugriff auf Daten. Nach außen stellt es sich oftmals als eine Art Liste von Domainobjekten dar. Spring Data arbeitet dabei deklarativ. Listing 4 zeigt den notwendigen Code für ein Repository von Events.
In einer Spring-Boot-Anwendung, die den entsprechenden Spring Data
Starter als Abhängigkeit deklariert, ist das alles, was Sie tun müssen,
um ein Repository dieser Art zur Laufzeit zu erhalten. Dieses Repository
müssen Sie nicht testen. In dieser Form gehe ich davon aus, dass das
Spring Data Team den Code getestet hat, der zur Laufzeit die
save
-Methode implementiert. Was ist aber mit dem Domain Service in
Listing 5, der sicherstellt, dass
keine doppelten Events gespeichert werden? Aus einem
datenbankzentrischen Perspektive kann das Thema natürlich mit einem
Unique-Constraint gelöst werden. Damit werden aber Verantwortlichkeiten
der Domain auf unterschiedliche Schichten verteilt und damit schlechter
sichtbar. Davon abgesehen müsste eine Verletzung des Contraints auch
entsprechend behandelt werden. Allerdings hält Sie niemand davon ab,
entsprechendes Constraint dennoch zu definieren, so wie in diesem
Projekt.
Die Klasse EventService
nutzt das Repository und eine der zur Laufzeit
bereitgestellten Query-Methoden:
An dieser Stelle sind zwei Dinge zu testen: Funktioniert die Methode
asExample
auf der Domain-Klasse wie erwartet und reagiert der Service
wie erwartet auf das Vorhandensein von Events. Um die Logik des Service
zu testen, nutze ich einen Mock. Ein Mock ist eine Attrappe, ein
Platzhalter der in Unit-Tests genutzt werden kann und so tut, als ob er
notwendige Funktionalität implementiert. Spring Boot stellt Ihnen im
entsprechenden Starter alle Werkzeuge zur Verfügung:
- Instruiert JUnit, die Tests mit Mockito auszuführen
- Das ist notwendig, damit automatisch “Attrappen” zur Verfügung stehen.
- Stellt das Szenario her: Die Attrappe des Repositorys wird so konfiguriert, dass die Suche nach dem “Halloween”-Event immer einen Treffer liefert.
- Die Attrappe ist nicht nur Platzhalter für einen anderen
Kollaborateur, sondern wird auch zur Überprüfung des Service-Code
genutzt: Wurde die Methode
findOne
tatsächlich aufgerufen?
Spring ist unter anderem eine Implementierung eines “Context- and
Dependency-Injection”-Containers. Die Kollaborateure des Services — im
Beispiel nur das Repository — werden von außen hereingereicht. Würde das
Repository über einen Aufruf von new
im Service selber erzeugt, könnte
ein Test wie oben nur über einen erheblichen Aufwand realisiert werden.
Dependency-Injection über Attribute ist mit Hinblick auf Testen auch
nicht zielführend und teilweise schädlich: Wie wird sichergestellt, dass
alle für einen Test benötigten Kollaborateure auch vorhanden sind? Wie
werden Kollaborateure ohne Setter-Methoden gesetzt?
Der Spring DI-Container nimmt Ihnen die Arbeit ab, Infrastruktur für Dependency-Injection selber zuschreiben. Die Erzeugung von Kollaborateuren ist vollständig von ihrer Benutzung entkoppelt. Da Spring keine Reflection-Hacks einsetzt sondern “nur” den Konstruktor des Service nutzt, erhalten Sie eine Klasse, die ohne Hacks testbar bleibt. Tatsächlich liegt auch in Listing 6 noch ein echter Unit-Test vor, da anstelle des Repositorys nur eine Attrappe genutzt wird und zu keinem Zeitpunkt der Spring-Container benötigt wird.
Gezielte Tests technischer Schichten
Spring Boot stellt Ihnen unter dem Begriff “Test-Slices” eine
Möglichkeit zur Verfügung, gezielt technische Schichten einer Anwendung
im Kontext des laufenden Spring Containers zu testen. Technische
Schichten sind zum Beispiel Datenbankzugriff oder der Weblayer. Für
Ihren Test hat das den großen Vorteil, dass er schlanker sein kann.
Möchten Sie gezielt eine REST-API testen, können Sie oftmals auf eine
echte Datenbank oder andere unterstützende Dienste im Hintergrund
verzichten. Gegeben sei die API in
Listing 7. Sie basiert auf Spring
Web MVC, das dominante Programmiermodell basiert auf Annotationen. Im
Beispiel besagen sie, dass unter der URL
/api/events/2017-12-24/Weihnachten
ein Event mit seinen Eigenschaften
abrufbar sein soll.
- Markiert die Klasse als Rest-Endpunkt.
- Gibt den Pfad
/api/events
als Basispfad für alle weiteren URLs dieser Klasse vor. - Bildet diese Methode auf einen Pfad unterhalb von
/api/events
ab, der durch zwei Pfadvariablen,heldOn
undname
parametrisiert ist. -
@PathVariable
ordnet die Methodenparameter der Variablen der URL zu.
Die API benötigt dazu den Event Service. An dieser Stelle möchte ich
sicherstellen, dass die Abbildung der Funktion auf URLs und die
Serializierung der Event-Daten korrekt funktioniert. Das Ziel ist, die
Integration der Komponenten EventsApi
und EventService
mit dem
Spring Web Framework zu testen. Spring Boot stellt Ihnen für diese
Schicht @WebMvcTest
zur Verfügung.
Listing 8 zeigt die
Verwendung:
- Hier wird ein spezieller Runner benötigt, der den Spring-Kontext startet.
- Damit der Test schneller startet, soll er nur die in Listing7 gezeigte Klasse beinhalten.
- Hiermit wird Spring REST Docs aktiviert, eine Möglichkeit, während des Tests automatisch eine API Dokumentation zu erzeugen.
- Anweisung, den
EventService
als Mock bereitzustellen. Dieser Mock wird in der nachfolgenden Methode konfiguriert, bei bestimmten Eingaben immer ein bestimmtes Ergebnis zu liefern - Aufruf der API
- Ausdruck des erwarteten Ergebnissen
- Hier wird eine Aktion formuliert, die nach Erfüllung aller
Erwartungen ausgeführt wird. Struktur der Rückgabe wird
dokumentiert. Die Dokumentation ist gleichzeitig Ausdruck einer
weiteren Erwartung: Das JSON-Dokument muss die Felder
heldOn
,name
und so weiter erwartet
Der Test in Listing 8 ist durchaus komplex: Es wird ein Spring-Kontext und das Web Framework gestartet; dennoch ist er lesbar und die Erwartungen sind klar erkennbar. Durch die Integration mit Spring REST Docs generiert er darüber hinaus eine dynamische Dokumentation der API, die aktiver Teil des Tests ist: Fallen die hier beschriebenen Felder auf einmal weg, bricht der Test. Die Dokumentation kann in verschiedenen Formaten generiert werden. Das Beispielprojekt nutzt das AsciiDoc-Format zur Generierung von Dokumentationsfragmenten, die in einer ausführlichereren Dokumentation eingebunden werden können.
Integrationstests
Bis hierhin wurden Unittests unterschiedlicher Schwierigkeit durchgeführt. Es musste umso mehr Infrastruktur bereitgestellt werden, je näher an der Anwendungsschicht getestet wurde. Trotzdem wurde der eigentliche Kontext des Event-Service nicht verlassen. Der erste Integrationstests mit Komponenten außerhalb des Spring-Kontexts wird mit der Deklaration einer Datenbankabfrage in Listing 9 benötigt. Diese Abfrage wurde zwar in JPQL, der Java Persistence Query Language aufgeschrieben und sollte damit einigermaßen portabel sein, aber Sie müssen trotzdem sicherstellen, dass sich keine Tippfehler eingeschlichen haben oder die geplante Zieldatenbank Ihre Abfrage umsetzen kann.
Es ist sinnvoll, Integrationstests von Unittests zu trennen; zum Beispiel erkennbar am Namen, besser noch durch separate Sourcen. Mit Gradle und dem eingebauten JUnit-Plugin ist das für eine Spring-Boot-Anwendung schnell und nachvollziebar gemacht, wie Listing 10 zeigt.
- Definition einer weiteren Menge von Quellen mit Namen
integrationTest
- Definition eines Tasks, der vom Test-Task erbt, aber auf anderen Quellen arbeitet
- Aktivierung eines Spring-Profiles, um passende Konfigurationseigenschaften zu aktivieren
Integrationstests gegen Datenbanken können heikel sein. Schwergewichtige Datenbanken wie eine Oracle-Datenbank nur für einen Tests zu starten, kann dauern. Gegen In-Memory-Datenbanken zu testen, bildet nicht die Realität ab. Eine Alternative ist die Generierung unterschiedlicher Schemas für Testzwecke. Das Beispielprojekt des Artikels geht den konsequenten Weg und startet die Zieldatenbank, eine PostgreSQL-Instanz, während der Integrationstests per Docker. Die Datenbank wird mit den Mitteln von Spring Boot initialisiert und der vollständige Test ergibt sich in Listing 11.
Durch den Einsatz einer klassenweiten JUnit-Regel, der Docker-Compose-Rule, kann Docker zusammen mit Docker Compose genutzt werden, um die benötigten, externen Systeme zu starten. Dieses System kann alle notwendigen Daten enthalten oder vom Spring-Kontext noch initialisiert werden.
Docker wird unter anderem dazu genutzt, Anwendungen aller Art in sogenannten Containern bereitzustellen, die innerhalb einer Betriebssystemvirtualisierung ablaufen. Der Einstieg ist sehr einfach, einzelne Anwendungen, beispielsweise Datenbanken, sind schnell bereitgestellt. Wird allerdings mehr als ein System verwaltet, steigt der Aufwand schnell an. Docker Compose ist eine Möglichkeit, eine Menge von Systemen innerhalb einer Konfiguration zu verwalten, gemeinsam zu starten, skalieren und zu stoppen. Michael Vitz von innoQ hat das Konzept unter dem Titel “Komplette Systeme mit Docker managen” ausführlich beschrieben.
In Listing 11 wird der
Test-Slice @DataJpaTest
benutzt, der die Datenbankschicht startet.
Möchten Sie auf Funktionalitäten Ihrer neuen Anwendung von externen
Systemen zugreifen, so nutzen Sie @SpringBootTest
. Damit fahren Sie
die Spring-Boot-Anwendung vollständig während eines Tests hoch. Eine
weitere, empfehlenswerte Variante für einen vollständigen Systemtest
ist, Ihre Anwendung und alle benötigten Services in einem Container zu
starten und diese von außen durch einen dezidierten Dienst zu testen und
so unter anderem Abhängigkeiten zwischen Integrationstests zu vermeiden.
Überprüfung der Testabdeckung
Als Testabdeckung wird das Verhältnis der getroffenen Aussagen eines Tests gegenüber den möglichen Aussagen bezeichnet. Dabei spielen unterschiedliche Kriterien wie Funktions-, Statement- und Zweigabdeckung oder auch Abdeckung von Bedingungen eine Rolle. Ein bekanntes Tool in der Java-Welt zur Messung der Testabdeckung ist ist JaCoCo. Listing 12 zeigt, dass JaCoCo mit nur wenigen Zeilen in Ihr Gradle Build File eingebaut werden kann.
Definieren Sie ein Mindestmaß an Testabdeckung, das Sie nicht unterschreiten möchten, aber zwingen Sie Ihre Entwickler nicht, unrealistisch hohe Vorgaben einzuhalten. Die Kosten überschreiten den Nutzen schnell und die Versuchung ist groß, Code und Tests zu produzieren, die die Abdeckung in die Höhe treiben ohne inhaltlich zu testen. Die Etablierung eines Testfundaments und davon ausgehender Durchstich durch die Ebenen einer Architektur ist wichtiger als eine möglichst hohe Menge.
Die Änderung der Testabdeckung in Relation zu neuem Code ist oftmals eine bessere Metrik zur Beurteilung von Qualität als der absolute Wert der Abdeckung.
Betrachten Sie lieber gültige Eingabebereiche für Module und Grenzfälle, anstatt immer alle Pfade durch eine Methode zwanghaft durchlaufen zu wollen. Nehmen Sie Daten, die zu Fehlern geführt haben, in Ihre Tests auf. Um die Qualität ihrer Tests selber zu verbessern kann Mutationstesting sinnvoll sein. Dabei wird während der Ausführung von Tests der zu testende Code mutiert, so dass es zu Fehlern kommt. Werden diese Fehler von Ihren Tests nicht erkannt, müssen die Testfälle anders gewählt werden.
Fazit
Die Testpyramide
Der Autor Alister Scott stellte 2012 den Begriff der Testpyramide und das dazugehörige Antipattern, das Testing-Eishörnchen vor.
Unit-Tests sind — die richtige Herangehensweise vorausgesetzt — einfach zu erstellen und sollten zahlreich vorhanden sein. Integrationstest — in vielfältigen Ausprägungen (in figure_title als API Tests, Integrationstests zwischen Systemen oder als Tests zwischen Komponenten) — sind in der Regel aufwendiger, und eine der Königsdisziplinen sind automatisierte UI-Tests, darüber sind nur noch Click-Tests durch echte Benutzer angesiedelt. Die sind in der Regel einfach durchzuführen, dadurch nicht weniger teuer. Leider sieht es in der Realität oftmals eher so aus, dass die “Wolke” an der Spitze übermässig groß ist und die Pyramide umgekehrt wird. Es werden immer noch viele manuelle und damit langsame und teure Tests durchgeführt.
Versuchen Sie, das Fundament Ihrer Anwendung, die fachlichen Anforderungen, so klar wie möglich herauszuarbeiten und klassisch mit Unit-Tests zu verifizieren. Ob Sie dabei tatsächlich immer hundertprozentig testgetrieben vorgehen, sei dahin gestellt. Tests müssen Vertrauen schaffen, auf Basis dessen fallen Refactorings und Erweiterungen leicht. Ob Tests physikalisch vor dem zu testenden Code existiert, ist zweitrangig.
Weitere Tests und Herausforderungen
Gerade in Hinblick auf das Thema continous delivery, also der kontinuierlichen Auslieferung von Bausteinen eines Systems, kommen weitere Testarten ins Spiel. Eine continous delivery pipeline bringt eine Software durch verschiedene Phasen kontinuierlich in Produktion. Diese Phasen beinhalten Performance-, Akzeptanz-, Kapazitäts- und auch explorative Tests.
Mein Kollege Eberhard Wolff spricht in dieser Hinsicht von Unendlichem Vertrauen. Akzeptanztests schaffen vertrauen beim Kunden, ob die Software ihre Anforderungen erfüllt und können als Fundament einer weiteren Testpyramide betrachtet werden, die sich der Software sozusagen von der anderen Seite nähert. Wichtig ist allerdings, auch diese Tests soweit wie möglich zu automatisieren, um das Ziel zu erreichen, eine Software möglichst schnell und im Falle von Änderungen auch möglichst oft in Produktion zu bringen. Unstrittig ist, dass es noch schwieriger ist, Kunden bei der Entwicklung automatisierter Tests mit ins Boot zu holen, aber die sich daraus ergebenden Vorteile sind den Aufwand wert.
Schlussendlich stehen und fallen Konzepte mit den Menschen dahinter. Der Wert von Unit- und Integrationstests muss ebenso erkannt und gelebt werden wie der von automatisierten Akzeptanztests, damit etwas wie continous delivery erfolgreich in einer Organisation umgesetzt wird.