Bei pentacor entwickeln wir für unterschiedlichste Kunden Softwaremodule, um ihre Geschäftsprozesse zu unterstützen und neue Möglichkeiten für durchgängige Digitalisierung zu erschließen. Die Implementierungen erstrecken sich dabei von einzelnen Diensten bis hin zu komplexeren Systemen bestehend aus mehreren Services und Single-Page-Applikationen in Kombination mit Lösungen zur Datenhaltung. Und all das ist dann noch eingebettet in die bestehenden, mal mehr, mal weniger komplexen Umgebungen unserer Kunden. Dabei haben wir es uns zur Aufgabe gemacht, jede unserer Softwarekomponenten automatisiert zu testen (einen Ausflug zur Testautomatisierung hat Ramon schon in seinem Blogeintrag Test-Ziele – Warum wir automatisiert testen veröffentlicht). Hier ist unser Ziel, eine qualitativ hochwertige und auch möglichst angemessene Testabdeckung zu erreichen.
Tests für unsere Lösungen lassen sich in zwei Kategorien unterteilen: White-Box-Tests und Black-Box-Tests. Doch wo liegen dabei die Unterschiede? Welche Tests gehören in die jeweilige Kategorie? Und welche Herausforderungen müssen wir in unseren Projekten bei der Umsetzung meistern? Genau das möchte ich gern in diesem Blog mit euch teilen. Also: „Are you ready to think outside the (white and black) box“? Ja? Dann lasst uns loslegen.
Black-Box-Tests
Mit Black-Box-Tests überprüfen wir typischerweise komplette Anwendungen. Das kann ein System aus mehreren Diensten oder auch einzelne Dienste sein. Dabei ist es nicht notwendig, die innere Struktur – also die Implementierung – zu kennen. Bei unseren Black-Box-Tests prüfen wir, ob die umgesetzte Anwendung entsprechend ihrer Spezifikation arbeitet. Diese umfasst zum einen fachliche Anforderungen oder User Stories und zum anderen auch technische Aspekte wie die Konformität gegen eine API-Spezifikation (OpenAPI 3.0). Darüber hinaus können wir durch diese Methode sehr gut aufdecken, ob sich die Entwickler der verschiedenen Dienste eines Systems richtig abgestimmt oder aneinander vorbei implementiert haben. Durch das Arbeiten in kleinen, dezentralen Teams wird das durchaus begünstigt und sollte deshalb beim Testen auch nicht außer Acht gelassen werden.
Lasst uns als Beispiel annehmen, wir hätten eine Softwarekomponente in Spring Boot mit einer REST-API und eine dazugehörige Single-Page-Applikation (SPA) in React bereitgestellt. Weiterhin kommuniziert die Softwarekomponente per REST noch mit anderen Systemen, die schon in der IT-Landschaft des Kunden existieren. Hier würden wir im Black-Box-Test zunächst unsere beiden Komponenten (das Backend und die SPA) getrennt voneinander testen. Dies wären dann API-Smoke-Tests gegen das Backend, um zu prüfen, ob die API überhaupt im Ansatz läuft und entsprechend der API-Spezifikation antwortet. Als nächste Stufe erstellen wir Systemintegrationstests, die andere bestehende, zu Testzwecken bereitgestellte Dienste oder auch Mocks dieser Dienste verwenden. Die UI der SPA ist dann über End-To-End-Tests mit gemocktem Backend oder auch das vollständige bereitgestellte System validierbar. Somit können wir prüfen, ob beide Komponenten entsprechend ihrer Spezifikationen funktionieren, aber auch ob sich das gesamte System inklusive der bestehenden Dienste im Zusammenspiel resistent und erwartet verhält.
Herausforderungen
Das klingt erstmal alles ganz toll, aber leider gibt es bei den Black-Box-Tests auch einige Herausforderungen, die oft nicht so leicht lösbar sind. Dies beginnt schon mit der Integration in vorhandene Authentifizierungs- und Autorisierungsmechanismen. Häufig nutzen SPAs heutzutage OAuth-Flows, aber selten gibt es dafür auch Testsysteme. Oft sind wir gezwungen diese Mechanismen nachzubauen oder wir können uns über umständliche Konfiguration wenigstens einen Testflow anlegen lassen. Meistens benötigen wir auch Testdaten, die für alle Systeme gültig sind. Wenn wir an unser Beispiel denken, dann könnte unsere UI ein Token an das Backend schicken, das wiederum Nutzerdaten (z.B. eine User UUID) bei einem Authentifizierungsdienst anhand des Tokens anfragt. Mit dieser User ID können dann aus den anderen bestehenden Diensten nutzerrelevante Daten gezogen und aggregiert wieder an die UI geliefert werden. Wie schaffen wir es dann, dass für unsere Tests die User ID überall bekannt ist und auch entsprechende Testdaten zurückgibt? Auch hier gestaltet sich die Realität oft sehr kompliziert: Wenn wir Glück haben, können sehr aufwändig entsprechende Daten bereitgestellt werden, die auch dann meist nur einen dedizierten Testfall abdecken, oder, wenn wir Pech haben, können wir diese Tests nur über eigene implementierte Mocks ermöglichen.
Vor- und Nachteile
Das komplette Test-Setup für das automatisierte Testen ist sehr aufwändig. Je nachdem, was die Tests (Testdaten, Testsysteme, eigene Mocks, etc.) benötigen, kann die Testkonfiguration sowie die Aufbereitung sehr komplex und zeitaufwändig werden.
Für End-To-End-Tests mit UI Integration ist es auch wichtig Headless-Browser–Funktionalität bereitzustellen, um eine Testautomatisierung zu ermöglichen. Hier gibt es zum Glück schon einige sehr gute Bibliotheken, die dabei unterstützen können, wie zum Beispiel protractor oder puppeteer. So schön es ist, den kompletten Ablauf zu testen, werden doch hier keine Missstände im Quellcode oder falsche fachliche Implementierungen aufgedeckt. Diese Fragestellungen decken aber die White-Box-Tests ab.
White-Box-Tests
Mit White-Box-Tests überprüfen wir genau das, was wir bei einer Black Box außer Acht lassen: die internen Strukturen und Funktionalitäten der eigenen, bereitgestellten Dienste. Dafür brauchen wir eine interne Sicht auf das System. Was bedeutet, dass der Code und der Test in einer engen Beziehung zueinander stehen und wir beide verstehen müssen. Nichtsdestotrotz testen wir nicht nur technische und programmatische Abläufe (wie zum Beispiel die Behandlung von Ausnahmefehlern) sondern auch fachliche Aspekte. In unserem Beispiel könnte das ein Test sein, der prüft, ob wir erhaltene Daten im Backend korrekt aggregieren oder im Fehlerfall eine sinnvolle Rückmeldung an unsere UI zurückgeben.
White-Box-Tests können in unterschiedlicher Granularität getestet werden. Wir bei pentacor unterscheiden zwischen Tests, die einzelne Einheiten, Methoden oder Klassen prüfen – sogenannte Unit-Tests – und Tests, die Pfade und Funktionalitäten zwischen Einheiten oder Subsystemen testen – sogenannte Module-Tests. Wenn wir hier an unser Beispiel aus dem Black-Box-Test denken, dann wäre ein klassischer White-Box-Test für das Backend zum Beispiel die Prüfung einer Aggregationskomponente, die Daten aus mehreren Quellen zu einem Objekt zusammenfasst. Diese Prüfung können wir dann auf Unit-Ebene durchführen, indem wir die Aggregationseinheit mit Testdaten aufrufen und das Rückgabeobjekt auswerten. Und als nächste Stufe würden wir den Test auf Modulebene ausführen, indem wir die REST-Anfrage an das Backend vortäuschen und das Ergebnisobjekt mit den aggregierten Daten überprüfen. Dabei durchläuft der Test nicht nur die Aggregationseinheit, sondern auch andere benötigte Komponenten zur Ausführung der REST-Anfrage und zur Erstellung des Ergebnisobjekts.
Ein Beispiel für White-Box-Tests der UI ist die Prüfung einer Suchmaske. Auf Unit-Ebene würden wir hier zum Beispiel die verschiedenen Eingaben prüfen und validieren. Dazu gehören Prüfungen wie: „Sind alle erforderlichen Eingaben gemacht?“ oder „Entsprechen die Suchmuster ihrer Spezifikation?“ Aber auch Validierungen der Darstellung einzelner UI-Elemente werden durchgeführt. Das können unter anderem Tests sein, die prüfen, ob alle Labels im korrekten Stil und ihr Text in der richtigen Sprache dargestellt werden.
Herausforderungen
Auf den ersten Blick scheinen White-Box-Tests eigentlich relativ klar und einfach. Das ist leider nicht so, denn auch hier sind wir regelmäßig mit Herausforderungen konfrontiert. Das beginnt oft damit, dass wir auch für diese Tests sinnvolle Testdaten benötigen. Diese müssen wir dann entweder selbst aus Spezifikationen der anderen Systeme bauen oder wir erhalten bestenfalls die Daten aus dedizierten Testsystemen.
Die nächsten Fragen, die wir uns dann stellen, sind: „Wie definieren wir die Grenzen unseres Tests?“, „Wo fängt eine sinnvolle, funktionale Trennung unserer Implementierung an und wo hört sie wieder auf?“ Vor allem bei den Module-Tests ist eine Antwort darauf nicht so trivial. Hat unsere Anwendung nur eine simple UI oder einen Microservice als Backend, können wir relativ schnell Abgrenzungen definieren. Werden die Dienste allerdings komplexer, dann ist eine Entscheidung für die Abgrenzung schwerer. Wir machen das bei uns dann häufig zur Teamaufgabe und definieren gemeinsam, wie wir die Testgrenzen strukturieren wollen.
Aber auch das Testen der Fehlerfälle ist herausfordernd. Neben den eigenen, programmierten Exceptions und deren Behandlung, müssen auch noch alle möglichen Fehlerfälle aus angrenzenden Systemen in unseren Komponenten geprüft werden. Wir müssen uns also nicht nur mit dem eigenen Code auseinandersetzen, sondern auch mit den anderen Systemen und deren Verhalten.
Vor- und Nachteile
Was sind also nochmal Vor- und Nachteile der White-Box-Tests?
Schlusswort
Beide Methoden, die wir Pentacornesen in unseren Systemen verwenden, haben ihre Vorteile und Nachteile. Allerdings gelten die Nachteile für uns nicht als Ausrede, diese Tests dann nicht zu implementieren. Ganz im Gegenteil – wir stellen uns der Herausforderung und finden eine Lösung, die zum einen für unseren Kunden aus zeitlicher und finanzieller Sicht sinnvoll ist und zum anderen uns nachts ruhig schlafen lässt. 🙂
Also stellt euch der Herausforderung: Be ready to think outside the box!
Bildquellen:
Daumen hoch: OpenClipart-Vectors auf Pixabay
Daumen runter: OpenClipart-Vectors auf Pixabay