Nachhaltige Report-Entwicklung mit TDD

Graphische Auswertungen sind ein zentraler Wertschöpfungsfaktor in den verschiedenen Softwareprodukten, mit denen wir uns im Entwickleralltag beschäftigen. Trotzdem verzichten viele Projektteams an dieser Schlüsselstelle auf automatisierte Tests. Wie kann ein Vorgehen aussehen, das dem hohen fachlichen Wert gerecht wird und dennoch in einem vertretbaren Zeitrahmen umsetzbar ist?

Stellen wir uns eine dieser zahlreichen Web-Applikationen vor: Ein schönes Backend mit einer Datenquelle und vielleicht noch einem Fremdsystem. Das Frontend ist eine SPA (Single-Page-Application) und es wurden in mehreren Sprints allerhand Features entwickelt. Im Wesentlichen verwaltet die Applikation Bug-Tickets, die in einem nicht sonderlich komplizierten Schema in einer relationalen Datenbank abgelegt sind. Moderne Frameworks und Bibliotheken ermöglichen eine Konzentration auf die Fachlichkeit, die testgetriebene Entwicklung läuft rund, das Projekt kommt gut voran. In der Story „Reporting“ liegen bereits dezidierte Layoutvorstellungen des Kunden für eine graphische Auswertung der gespeicherten Daten. Hier soll sortiert und aggregiert werden, Topdatensätze in einer Liste angezeigt, und ein Diagramm ist auch noch gewünscht. Das Ganze soll als mehrseitiges PDF ausgegeben werden (Abb. 1). Um die Anforderungen sinnvoll testen zu können, ist natürlich ein vernünftiges Set an Testdaten notwendig.

 

Unter Zugzwang

Bis dieses Set vorliegt, wird die Story fröhlich weitergeschoben, bis sie in einem Sprint-Planungsmeeting vom Kunden priorisiert wird. Hintergrund ist eine Präsentation in seiner Fachabteilung, die sich von dem Feature die zentrale Wertschöpfung innerhalb des Projekts erhofft.

Blenden wir uns kurz aus dem Gedankenspiel aus: Ist das nicht ein klassisches Dilemma des Projektalltags? In der Entwicklung sind wir glücklich und zufrieden, wenn die Daten ordentlich abgelegt sind, in einem sauberen relationalen Schema, einem Graphen oder einer beliebig skalierbaren NoSQL-Datenbank. Wir wissen um den Wert, den die Daten dort haben. Leider fehlt den Anwendern unserer Produkte diese Sicht. Für sie setzt die Wertschöpfung erst in dem Moment ein, in dem es uns gelingt, die Rohdaten aufbereitet zu präsentieren – in einer Liste oder eben einem Report, der schon gewisse Operationen darauf ausführt.

 

Ungeklärte Fragen

Zurück zum Beispielprojekt: Durch die Anforderung sieht sich das Projektteam nun mit der immer weiter geschobenen Reporting-Story konfrontiert und fühlt sich plötzlich in die Enge getrieben. Denn an der überschaubaren Datenbasis in der lokalen Entwicklungsumgebung hat sich nicht viel geändert. Da natürlich zeitgemäße Architekturentscheidungen getroffen wurden, wird ein Datenbankmigrations-Tool genutzt. Die lokale Umgebung läuft gegen eine Datenbank im Container, welche regelmäßig zurückgesetzt wird. Die Unit-Tests werden selbstverständlich gegen eine In-Memory-Datenbank ausgeführt. Woher sollen denn jetzt plötzlich ausreichend viele Testdaten kommen?

Seien wir mal ehrlich: Niemand entwickelt diese Reports gerne. Allein der Gedanke daran, sich über die Web-Oberfläche Testdaten zusammen zu klicken, dann auf dem Papier oder mit Excel die Aggregationen durchzurechnen, Implementieren, Projekt neu bauen, anmelden, durchklicken bis zum Report, generieren, warten, Feststellen, dass man einen Rundungsfehler gemacht hat, und das Ganze wieder von vorne. Zwischenzeitlich setzt man aus guter Gewohnheit wieder den lokalen Datenbankcontainer zurück und fügt damit dem Drama ein weiteres Kapitel aus dem Bereich „Daten zusammenklicken“ hinzu.

Natürlich gäbe es auch die Testumgebung. Die Piplines sind auch so eingestellt, dass bei jedem Push in den Feature-Branch gebaut wird, aber die Feedback-Zeiten (um dann am Schluss nur den Rundungsfehler festzustellen) sind dann doch abschreckend.

Die Variante, ein im Test generiertes PDF einzulesen und mit gewissen Erwartungen abzugleichen scheidet auf Grund der überschaubar positiven Erfahrung in anderen Projekten von vornherein aus. Es scheint so, als müssten die bisherigen Best-Practices im Projekt über Bord geworfen und an dieser zentralen Stelle auf die testgetriebene Entwicklung verzichtet werden.

 

Frische Ideen

Abgesehen von den ungeklärten Fragen ist natürlich das Ziel, den Report nicht in einem Eine-Klasse-eine-Methode-Monster zu bewältigen, sondern eine sinnvolle Aufteilung zu wählen, die den Best-Practices in Sachen Clean-Code gerecht wird. Erste Idee ist hier, die Klassen mit einem spezifischen Zweck auszustatten. Im vorliegenden Fall macht die Trennung in Datenabfrage/Aufbereitung und Layout Sinn. Plötzlich keimt Hoffnung auf: Ist bei der Wahl eines entsprechenden Designs eine testgetriebene Entwicklung doch möglich? Dies würde in den Projektkontext passen und wäre vor allem nachhaltig. Klar, auch hier müsste man den Aufwand der Testdatengenerierung einmal gehen, aber für automatisierte Tests wäre die Motivation deutlich größer.

Bei der Sprint-Planung folgt die obligatorische Diskussion und natürlich nagt der Zweifel: Die Daten liegen doch tabellarisch vor. Mit einer einfachen Query wäre die Aggregation getan und dann müsste man nur noch die Top 5 ins PDF pressen. Warum also noch mehr Struktur einführen? Ist hier in Anbetracht des Zeitdrucks nicht das klassische Vorgehen mit Sichttests ausreichend?

Andererseits würde dieses einfache Vorgehen das bisher im Projekt praktizierte testgetriebene Vorgehen massiv untergraben. Regressionsfehler wären erfahrungsgemäß unvermeidbar. Die Frage lautet also, wie viel automatisiertes Testen ist fachlich sinnvoll und wie viel ist wirtschaftlich möglich?

 

Von der Idee zum Design

Bei genauerer Betrachtung nimmt die anfangs noch vage Idee weiter Gestalt an. Wenn die gesamte Verantwortung für die Transformation aus der Datenbank gelesener JPA-Entities in ein als Byte-Array vorliegendes PDF-Dokument auf zwei Komponenten aufgeteilt wird, benötigen diese ein definiertes Datenformat für den Austausch. Dieses würde die Struktur der im Report dargestellten Daten repräsentieren, wäre also ein abstraktes Modell des PDFs. Konsequent weiter gedacht eröffnen sich die Möglichkeiten, die eine Vielzahl der zuvor aufgeführten Probleme lösen: Zumindest die Erstellung des Modells ist optimal testgetrieben entwickelbar. Die Geschäftslogik ist schön in den jeweiligen Komponenten gekapselt, die das Modell erstellen und die das Modell zur Ausgabedatei transformieren. Das Modell kann aus POJOs zusammengesetzt werden und enthält selbst keinerlei Logik (Abb. 2).

Die Modellierung sinnvoller Testfälle zwingt zu einem frühen Zeitpunkt zu einer Auseinandersetzung mit den fachlichen Kundenanforderungen. Für den Geschäftswert wichtige Grenzfälle können einfach und automatisiert getestet werden.

 

 

Am Anfang die Modellierung

In der Praxis wird im ersten Schritt die Kundenanforderung anhand der Vorgaben in ein abstraktes Modell gefasst. Das Beispielprojekt1 zeigt die Umsetzung des, zugegebenermaßen minimalistischen, Bugreports aus (Abb. 1) für einen bestimmten Monat. Die Seite enthält die fünf Bugs mit der größten Auswirkung tabellarisch angezeigt sowie ein Tortendiagramm mit einer Übersicht über die Kategorien der aufgetretenen Bugs.

  

(Listing 1)

Wie in (Listing 1) ersichtlich, wurde die Entscheidung getroffen, die Überschriften über Tabelle und Diagramm mit in die jeweiligen Modelle einfließen zu lassen. (Listing 2) zeigt das Modell für das gesamte Tortendiagramm, im Wesentlichen ist hier die Überschrift und eine Liste der Daten für die einzelnen Sektoren enthalten.

 

(Listing 2)


(Listing 3)


Aus (Listing 3) wird ersichtlich, dass teilweise Layoutinformationen in das Modell aufgenommen werden: Da die Farbe der Sektoren eine fachliche Anforderung ist, erscheint sie hier im Modell. Mit diesen wenigen Codezeilen ist die gesamte rechte Spalte des Reports modelliert.

Die Modellierung von Tabellen ist durch die Zweidimensionalität komplizierter: Es hat sich jedoch das im Beispielprojekt gezeigte Vorgehen bewährt, in einer Map eindeutige Schlüssel für die Spalten und deren Layoutinformation (Überschrift, Breite, Ausrichtung, etc.) zu hinterlegen. Die Werte für die Zeilen der Tabelle stehen in einer Liste, die Map-Elemente aus Spaltenschlüssel und dem Zellwert enthält. Hierdurch ist das Umsortieren von Spalten einfach und ohne Indexschlacht möglich, insbesondere ist die Aufbereitung des Modells über die Spaltenschlüssel transparent.

 

Endlich wieder TDD

Steht die Struktur des Modells, können die Tests geschrieben werden. Hierzu muss zunächst ein minimales, aber sinnvolles Set an Testdaten erstellt werden. Dieses kann beispielsweise per JPA-Repositories oder mit einem SQL-Script vor jedem Testlauf in der (In-Memory-) Datenbank angelegt werden. Wie bei allen anderen Unit-Tests ist es wichtig, das richtige Maß zu finden: Kurze, aussagekräftige Tests definieren, die bei einem Fehlschlag möglichst schon im Namen sagen, welche Erwartungen verletzt wurden. Ein besonderes Augenmerk ist auf spezielle, fachliche Anforderungen zu setzen: Im Beispiel des Bug-Reports sollte sichergestellt werden, dass tatsächlich kein Bug-Eintrag aus dem Vormonat in der Tabelle landet, obwohl er einen hohen Impact-Wert hat und, dass im Tortendiagramm nur Kategorien angezeigt werden, in denen im ausgewerteten Monat wirklich Bugs erfasst wurden.

 

(Listing 4)


Mit den Unit-Tests ist die Implementierung der Hilfsmethoden aus (Listing 4) reine Schreibarbeit. Je nach Umfang der Datengrundlage müssen auch Performanceüberlegungen angestellt werden: Wie in den meisten anderen Fällen macht es Sinn, Filterung und Sortierung bereits bei der Datenbankabfrage erledigen zu lassen. Durch die hohe Testüberdeckung ist aber ein späteres Refactoring relativ unproblematisch.

 

Mit Fokus

Sobald die Modellstruktur fixiert ist, kann parallel bereits mit der grafischen Umsetzung in Richtung PDF begonnen werden. Hierzu ist schnell ein Beispielmodell geschrieben, das als Datengrundlage für die an dieser Stelle unvermeidlichen Sichttests dient. In der täglichen Arbeit zeigt sich hier neben der Parallelisierbarkeit der zentrale Vorteil des beschriebenen Designansatzes: die beiden Hauptservices (ModelService und PdfService) lösen jeweils nur eine Fragestellung, diese aber sehr gut. Diese Fokussierung hilft bei der Entwicklung ungemein und führt dazu, dass Fehler schnell lokalisiert werden können. Wenn die Sicherheit besteht, dass das Tabellenmodell korrekt ist, aber im PDF nur vier anstatt der erwarteten fünf Zeilen angezeigt werden, muss der Fehler in der Umsetzung zur PDF-Datei liegen.

 

Nachhaltig erfolgreich

In der täglichen Arbeit stellt sich schnell das Gefühl ein, Struktur in die sonst so unübersichtliche Thematik gebracht zu haben. Die Klassen sind kürzer, die Tickets kleiner und das Entwicklungstempo ist konstant hoch. Die Implementierung weiterer spezialisierter Services, zum Beispiel zur Umsetzung des Diagramms und der Tabelle fängt an, Freude zu machen. Und auch langfristig erweist sich der Ansatz als gute Wahl: Fehler im Testbetrieb können oft lokal als Regressionstests nachgestellt werden und sind schnell und nachhaltig behoben. Die Endanwender können zeitnah mit einem verlässlichen Stand arbeiten, es werden kaum Bugfix-Deployments gebraucht. Wenn nach einem Jahr der gleiche Export nicht nur nach PDF, sondern auch in Word gewünscht wird, ist das kein Problem. Schnell ist eine weitere Komponente programmiert, die die gleiche Modellstruktur in Word überführt. Die Modellgenerierung wird dabei nicht mehr angefasst, daher sind die angezeigten Daten vom ersten Tag an korrekt. Durch die Entkopplung ist es ebenso leicht möglich, die Bibliothek zur PDF-Generierung gegen eine andere auszutauschen. Beim Austausch genügt die Konzentration auf das „Wie werden die aufbereiteten Daten angezeigt?“.

 

Fazit

Zwei Erkenntnisse bleiben: Zum einen, dass es möglich ist, durch ein gelungenes Design die testgetriebene Entwicklung zu ermöglichen. Dieser testfokussierte Ansatz ist ein Paradigmenwechsel, hat sich aber, nicht nur im hier geschilderten Anwendungsfall bewährt. Der Wert der Software und damit auch der Erfolg des Designs bemisst sich klar aus dem Maß der automatisierten Testbarkeit. Zum anderen gilt der Leitsatz: „Man muss nicht alles automatisiert testen – nur die richtigen Dinge“. Nur bis zum Modell automatisiert zu testen – hier jedoch auf eine hohe Überdeckung wert zu legen – hat sich klar bewährt. Es ergibt sich ein immer optimaleres Verhältnis aus dem Anspruch an Qualität gegenüber dem Budgetdruck. Auch in kleinen Projekten kann man auf diesem Weg mit ausreichender Sicherheit durch automatisierte Tests aufwarten. Die Suche nach einem sauberen Design hat ganz nebenbei eine Idee eingeführt, die für viele Anforderungen eine Hilfestellung ist. Neben grafischen Reports gibt es noch viele weitere sinnvolle Einsatzfelder: Der Versand von HTML-E-Mails gehört genauso dazu, wie listenbasierte Exporte mit komplizierter Geschäftslogik.

 

Julius Mischok ist Geschäftsführer der Mischok GmbH in Augsburg. Seine Kernaufgaben sind Prozessentwicklung, sowie Coaching und Schulung der Entwicklungsteams. Aktuell fokussiert sich seine Arbeit auf die Frage, wie Software schnell und mit einer maximalen Wertschöpfung produziert werden kann. Er hat Mathematik stufiert und entwickelt seit fast zwei Jahrzehnten Java.

 

[1] https://gitlab.mischok-it.de/open/reports

Redaktion


Leave a Reply