
Pyramiden sind uns allen ein Begriff. Die bekanntesten sind vermutlich die Pyramiden, die ägyptische Pharaonen als Begräbnisstätten und Abbild der Hierarchie der Sozialstruktur errichten ließen. Pyramiden wurden jedoch unabhängig und völlig selbstständig von weit voneinander entfernt lebenden Volksstämmen errichtet. Heute kennen wir Pyramiden aus der ganzen Welt: Ägypten, Lateinamerika, China, Griechenland, Rom, … Ihnen gemein ist, dass es sich vorwiegend um Gebäude mit religiösem und/oder zeremoniellem Charakter handelt, beispielsweise als Teil eines Totenkults.
Doch was genau hat das nun mit Software und dem Testen von Software zu tun? Handelt es sich dabei etwa auch um ein Konstrukt mit zeremoniellem oder religiösem Charakter? Die Antwort auf diese Frage hängt wahrscheinlich sehr davon ab, wen man fragt … Handelt es sich gar um einen Totenkult? Hoffentlich nicht – unsere Software soll leben!
Und genau dafür leistet eine ganz andere Pyramide einen wichtigen Beitrag: die Test-Pyramide.
In der mythologischen Deutung von Stufenpyramiden handelte es sich um eine riesige Steintreppe zum Himmel, dem “Haus der Ewigkeit” für einen König, das ihn Unsterblichkeit erlangen ließ. Unsterblichkeit klingt gut, das wünschen wir uns für unsere Software doch vielleicht auch. Allerdings sollten wir das Thema vielleicht nicht so angehen wie Cheops, der böse Pharao, der alle Untertanen zwang, beim Pyramidenbau zu helfen.
Unsere Motivation, beim Bau der (Test-)Pyramide mitzuwirken, finden wir womöglich in dem Nutzen, den die Menschen in Lateinamerika aus ihren Pyramiden zogen: Bei den dortigen Pyramiden handelte es sich um den Unterbau belebter Hochebenen, auf dem Häuser, Paläste und Tempel errichtet wurden. Diese Bauwerke waren dadurch vor Überschwemmungen während oft heftiger Regenfälle geschützt. Dieser Schutz ist potenziell lebensnotwendig.
Gleiches gilt für Tests bei der Softwareentwicklung – unsere Test-Pyramiden. Zugeben, stellenweise gleichen sie – trotz ihrer Wichtigkeit – eher Grabhügeln, deren konsequente architektonisch-bautechnische Weiterentwicklung die ägyptischen Pyramiden waren. Gleichen die Tests in eurem Projekt also noch eher einem Hügel als einer Pyramide, seid unbesorgt – ihr seid nicht die ersten (und wohl auch nicht die letzten), denen es so geht. Und es gibt Hoffnung. Auch die vielen Pyramiden in Lateinamerika wurden manchmal sogar mehrfach überbaut.
Pyramiden-Stufe 1: Unit-Tests

Beginnen wir unsere Betrachtung der Software-Test-Pyramide also bei kleinen Hügeln, von denen wir ohne großen Aufwand sehr viele errichten können, um das weitere Bauwerk auf eine breite Basis zu stellen. Beginnen wir bei den Unit-Tests. Vielleicht sind sie als kleinste Elemente unserer Test-Pyramide sogar deren Bausteine. Unit-Tests prüfen ein einzelnes, individuelles Stück Code. Das kann zum Beispiel eine Komponente oder Funktion sein. Grundlegend gilt für dieses Stück Code, dass es sich um die kleinste testbare Einheit handelt, für die es nur wenige Eingabewerte und üblicherweise ein einzelnes Ergebnis gibt.
Alles, was nicht im Scope dieser einzelnen “Unit” ist, wird gemockt, gestubbt oder durch Fake-Objekte ersetzt. Ziel dabei ist die Isolation des zu testenden Codes, so dass klare und eindeutige Ein- und Ausgabewerte gewährleistet sind. Idealerweise werden solche Tests geschrieben, bevor der zu testende Code überhaupt existiert. Robert C. Martin (“Uncle Bob”) hat drei Gesetze für derart testgetriebene Entwicklung aufgestellt:
- Du darfst produktiven Code erst dann schreiben, wenn du einen fehlschlagenden Unit-Test dafür geschrieben hast.
- Der Unit-Test darf nicht mehr Code enthalten, als für korrektes Kompilieren und Fehlschlagen des Tests erforderlich ist.
- Schreib nur so viel produktiven Code wie notwendig ist, damit der bisher fehlschlagende Test erfolgreich durchlaufen wird.
Das geht schnell. Und schnell hat man auf diese Weise sehr viele Tests, die parallel zur Entwicklung entstehen und uns jederzeit Auskunft darüber geben können, ob eine Änderung etwas kaputt gemacht hat. Daher ist es wichtig, sich in den Unit-Tests auf einen einzelnen Aspekt, ein einzelnes Konzept zu konzentrieren oder sogar nur eine Assertion zu verwenden. Schlägt ein Unit-Test fehl, soll unmittelbar deutlich werden, was genau nicht funktioniert hat und kaputt gegangen ist. Auf diese Weise schnell und einfach entdeckte Fehler können in späteren Test-Phasen nur sehr viel schwieriger aufgedeckt werden.
So gehören diese Tests auch unbedingt zum Build-Prozess und sollten grundsätzlich oft und regelmäßig ausgeführt werden, um schnell und regelmäßig Feedback zu bekommen.
Logischerweise schreiben wir als Entwickler die Unit-Tests selbst, für uns und andere Entwickler. Müssten wir darauf warten, dass erst jemand (ein Tester) Unit-Tests für uns schreibt, wäre der Vorteil der Geschwindigkeit dahin. Insbesondere angesichts der Tatsache, dass unter Umständen für eine Zeile produktiven Code deutlich mehr Test-Code entsteht, wäre das ziemlich absurd. Das macht gleichzeitig deutlich, dass es ratsam ist, sich auf das Testen von Komponenten zu konzentrieren, was auch Auswirkungen auf das Verhalten der Software als Ganzes hat. So ist es möglicherweise wenig sinnvoll, jeden Getter und jeden Setter eines Objekts zu testen. Der Mehrwert von Tests ist sehr viel größer, wenn wir uns darauf konzentrieren bei Schleifen und Entscheidungen alle Pfade abzudecken. Dabei sollten wir realistische Daten als Eingabewerte verwenden, um spätere Überraschungen zu vermeiden.
Berücksichtigen wir all das, haben wir ganz nebenbei noch einen weiteren, nicht zu vernachlässigenden Vorteil: Unsere Tests dokumentieren das von uns erwartete Verhalten. Wir – und alle, die vielleicht später zum Projekt dazukommen und mit unserem Code und den dazugehörigen Tests konfrontiert werden – können damit besser nachvollziehen, was wie funktioniert (und warum).
Das alles bezieht sich jedoch nur auf die in den Unit-Tests einzeln für sich getesteten Komponenten. Jeden Bug und jeden Fehler werden wir nicht finden. Und wir werden wahrscheinlich auch keine Probleme identifizieren können, die erst bei der Integration, also beim Zusammenspiel mit anderen Komponenten entstehen. Dafür gibt es die nächste Stufe unserer Test-Pyramide.
Pyramiden-Stufe 2: Modul-Tests & Integrationstests

Wir begeben uns ein Stück weiter nach oben in unserer Pyramide. Die kleinsten testbaren Einheiten erachten wir als ausreichend getestet und widmen uns nun ihrem Zusammenspiel. Bis hierher haben wir uns mit den Unit-Tests, die idealerweise parallel zur Implementierung entstanden sind, insbesondere mit internen Strukturen und Funktionalitäten auseinandergesetzt. Damit waren wir mit den Unit-Tests im Bereich der White-Box-Tests unterwegs.
Widmen wir uns den Modulen (manchmal auch als Komponenten bezeichnet), kann das Wissen zu derartigen internen Aspekten hilfreich sein, ist bei der konkreten Ausgestaltung der Tests aber gleichzeitig eine Frage von Ermessen und Teststrategie. Wir begeben uns allmählich ins Gebiet der Black-Box-Tests. Mit diesen beiden Ansätzen beim Testen von Software haben wir uns vor einiger Zeit bereits eingehender beschäftigt:
Bei den Modul-Tests können wir auf der einen Seite mehrere kleine Einheiten als zusammengehörig betrachten und gemeinsam als Gruppe testen. Das kann beispielsweise eine komplexere Funktionalität in einem Service einer Anwendung sein. Andere Teile der Anwendung werden dabei durch Mocks oder Stubs ersetzt, die für alle gerade nicht relevanten Aspekte der Anwendung klar definiertes Verhalten festlegen, ohne dabei auch direkt von deren tatsächlicher Implementierung abzuhängen (und sofort von Änderungen betroffen zu sein). Für Spring-Boot-Anwendungen ist es zum Beispiel möglich, einzelne Layer der Anwendung separat zu testen. Dabei wird für die Ausführung der Tests ggf. nur ein Teil der Anwendung tatsächlich initialisiert und ausgeführt, was sich positiv auf die Laufzeit der Tests auswirkt. Das ist wichtig, weil wir nach wie vor schnell und oft Feedback bekommen wollen und die Modul-Tests entsprechend auch automatisiert als Teil des Build-Prozesses ausführen.
Andererseits ist es auch denkbar, die gesamte Anwendung als Modul (eines größeren Systems) zu betrachten und damit als Gesamtes, als Black Box, zu testen. In diesem Fall würden wir Mocks und Stubs nur für externe Abhängigkeiten verwenden, die nicht direkt Teil unserer Anwendung sind. Das kann zum Beispiel die Interaktion mit einem Authentication Server oder Identity Provider sein. Für die Testausführung kann die Anwendung lokal gestartet werden und mit Bibliotheken wie REST-assured “von außen” aufgerufen werden. Dadurch durchlaufen die Tests auch alle Sicherheitsmechanismen, die für die Anwendung vorgesehen sind, während andere Möglichkeiten der Modul-Tests erst später ansetzen und damit die ersten Ebenen des Request-Handlings überspringen oder umgehen.
Egal, ob man sich auf komplexere Funktionalität innerhalb einer Anwendung oder eine Anwendung als Gesamtes konzentriert, steht das Aufdecken von Fehlern im Fokus, die bei der Integration und Interaktion verschiedener kleiner Einheiten unserer Software entstehen. Wir wollen sicherstellen, dass alles richtig verbunden ist und zusammen funktioniert.
Natürlich können wir dabei nicht jedes denkbare und undenkbare Szenario abdecken. Wir gehen stattdessen davon aus, dass die Mehrzahl potenzieller Fehlerfälle bereits in den Unit-Tests Berücksichtigung fand. Unser Augenmerk richten wir daher nun auf den “Happy Path” sowie offensichtliche “Corner Cases”. Das könnten zum Beispiel in der Vergangenheit aufgetretene Bugs sein, die nun explizit mit in die Tests aufgenommen werden.
An den beiden Ansätzen für Modul-Tests (oder Integrationstests) wird deutlich, dass der Begriff der Integrationstests sehr breit ist, was sich auch in verschiedenen Definitionen des International Software Testing Qualifications Board (ISTQB) widerspiegelt:
- integration testing: Testing performed to expose defects in the interfaces and in the
interactions between integrated components or systems. See also component integration
testing, system integration testing.
- component integration testing: Testing performed to expose defects in the interfaces and
interaction between integrated components.
- system integration testing: Testing the integration of systems and packages; testing
interfaces to external organizations (e.g. Electronic Data Interchange, Internet).
Das sorgt oft für Verwirrung und so richtig klar können Modul-Tests, Komponenten-Tests, Integrationstests und System-Integrationstests nicht gegeneinander abgegrenzt werden. Der Übergang ist tatsächlich fließend. So unterscheidet Martin Fowler schlicht und einfach zwischen “Narrow Integration Tests” und “Broad Integration Tests”, die sich wie folgt unterscheiden: