Dirk Richter
Software-Entwicklung und Architektur

Teil 1 - Hintergrundwissen zu Unittests im Allgemeinen


Inhaltsverzeichnis

Zusammenfassung

  • Der Text erklärt die Bedeutung und Ziele von Komponententests (Unit-Tests) in der Softwareentwicklung.
  • Er stellt das V-Modell und die verschiedenen Teststufen (Komponenten-, Integrations-, System- und Abnahmetest) vor.
  • Die Testpyramide von Mike Cohn wird als Leitbild für eine effiziente Teststrategie erläutert.
  • Es werden Anforderungen an testbaren Code und typische Anti-Patterns wie das Eisbecher-Anti-Pattern beschrieben.
  • Die Folgen schlechter Testbarkeit und Maßnahmen zur Verbesserung werden aufgezeigt.
  • Abschließend werden Ziele und konkrete Maßnahmen für den erfolgreichen Einsatz von Komponententests zusammengefasst, um Softwarequalität, Wartbarkeit und Regressionsschutz zu sichern.

Einleitung

In der modernen Softwareentwicklung spielen Tests eine unverzichtbare Rolle, um die Qualität, Stabilität und Wartbarkeit von Software zu gewährleisten. Insbesondere Unit-Tests – die sich auf das isolierte Testen kleiner, funktionaler Einheiten innerhalb eines Softwaresystems konzentrieren – ermöglichen es, Fehler frühzeitig zu erkennen und den Entwicklungsprozess effizienter zu gestalten.

Trotz ihrer Bedeutung herrschen in vielen Projekten Unklarheiten und Missverständnisse darüber, wie sich die unterschiedlichen Tests unterscheiden und welche Ziele sie verfolgen.
Begriffe wie „Komponententests“ und „Unit-Tests“ werden häufig synonym verwendet, obwohl sie sich in ihren Zielsetzungen oft unterscheiden.
Ebenso entwickeln Unternehmen und Teams im Laufe der Zeit eigene Definitionen und Ansätze, was zu Abweichungen von etablierten Standards führen kann.

Dieser Artikel hat das Ziel, die Grundlagen und Ziele von Tests klarer zu definieren und dabei auf anerkannte Standards wie jene des ISTQB - International Software Testing Qualifications Board zurückzugreifen. Dadurch soll ein fundierter Überblick geschaffen werden, mit dem sich firmenspezifische Ansätze vergleichen und weiterentwickeln lassen.

Ein besonderes Augenmerk wird darauf gelegt, wie Unit- oder genauer Komponenten-Tests dazu beitragen können, die Wartbarkeit, Stabilität und langfristige Qualität von Software zu fördern.

Hinweis: Im Folgenden werden Unit-Test konkret nur noch Komponenten-Test genannt, damit es nicht zu Verwechslungen kommt.

Die Bedeutung von Tests in der Softwareentwicklung

Die Bedeutung von Tests
Unit-Tests ermöglichen insbesondere:

  • Frühe Fehlererkennung: Fehler werden bereits während der Implementierung aufgedeckt und können zeitnah behoben werden.
  • Überprüfung der Funktionalität: Tests stellen sicher, dass einzelne Komponenten wie spezifiziert funktionieren und die definierten Anforderungen erfüllen.
  • Regressionssicherheit: Sie verhindern, dass Änderungen am Code unbeabsichtigte Nebeneffekte oder Fehler verursachen.
  • Unterstützung der Wartbarkeit: Tests erleichtern Refactorings und fördern eine saubere Architektur.
  • Vertrauen in die Software: Ein transparenter Teststatus (stets grün) und eine hohe Testabdeckung geben Aufschluss über die Qualität und erleichtern Freigabeentscheidungen.

Das V-Modell

Das Verständnis vom Softwaretest wird durch das V-Modell nachhaltig geprägt.
Die Grundidee des V-Modells ist, dass die Planung und die Entwicklung von Software mit den Tests zueinander korrespondierende, gleichberechtigte Tätigkeiten sind.

V-Modell

(V-Modell)

Je nach Detailgrad des Entwurfs und der zugehörigen Tests werden hier unterschiedliche Teststufen definiert


Teststufen

Die Teststufen gliedern sich entsprechend in spezifische Phasen, die im ISTQB-Standard festgelegt sind. Jede Stufe verfolgt verschiedene Ziele, Verantwortlichkeiten und Detailebenen, um sicherzustellen, dass Software umfassend getestet wird.

1. Komponententest

  • Beschreibung: Der Komponententest ist die niedrigste Ebene im Testprozess. Er konzentriert sich auf das isolierte Testen kleiner Softwareeinheiten wie Funktionen und Klassen.
  • Ziele:
    • Sicherstellen, dass jede einzelne Komponente spezifikationsgemäß funktioniert.
    • Überprüfung der internen Logik, Kontrollstrukturen und Schnittstellen.
  • Merkmale:
    • Häufig White-Box-Tests (Test der internen Code-Logik).
    • Verwendung von Stubs, Mocks und Fakes, um externe Abhängigkeiten zu simulieren.
  • Typische Testarten:
    • Datentests (z. B. Validierung von Eingaben und Ausgaben).
    • Tests von Randfällen und Fehlerbehandlungen.
  • Beispiele:
    • Komponenten-Test einer Funktion, die eine mathematische Berechnung durchführt.
    • Überprüfung einer Klasse, die Kundendaten verwaltet.

2. Integrationstest

  • Beschreibung: Der Integrationstest prüft, ob mehrere Komponenten korrekt zusammenarbeiten, üblicherweise solche, die direkt miteinander kommunizieren.
  • Ziele:
    • Prüfung der Interaktion zwischen direkt verbundenen Komponenten.
    • Sicherstellen, dass Daten korrekt übergeben und die Module ordnungsgemäß zusammenspielen.
  • Merkmale:
    • Tests auf mittlerer Abstraktionsebene: Fokus liegt nicht auf der Detail-Logik einzelner Komponenten, sondern auf ihrer Zusammenarbeit.
  • Typische Testarten:
    • Schnittstellentests: Überprüfung, ob die Datenflüsse zwischen Klassen und/oder Modulen korrekt sind.
    • Validierung von API-Kommunikation.
  • Beispiele:
    • Testen einer Klasse, die eine Anfrage an eine externe Komponente (z. B. Datenbank-Interface) sendet.
    • Sicherstellen, dass eine Suchfunktion eine korrekte Liste von Ergebnissen aus Datenbank-Abfragen zurückgibt.

3. Systemtest / Systemintegrationstest

  • Beschreibung: Der Systemtest umfasst die Überprüfung der gesamten Anwendung als ein funktionierendes System. Er evaluiert, ob das Gesamtsystem alle funktionalen und nicht-funktionalen Anforderungen erfüllt.
  • Ziele:
    • Sicherstellen, dass das System vollständig und spezifikationsgemäß funktioniert.
    • Identifizieren von Fehlern, die in vorherigen Teststufen nicht entdeckt wurden.
  • Merkmale:
    • Black-Box-Tests dominieren: Fokus liegt auf den spezifizierten Anforderungen aus Anwenderperspektive.
    • Prüfung der Funktionalität, Sicherheit, Leistung und Zuverlässigkeit.
  • Typische Testarten:
    • Funktionale Tests: Überprüfung des Systemverhaltens gemäß den Anforderungen.
    • Nicht-funktionale Tests: Performance, Usability, Security.
  • Beispiele:
    • Testen des gesamten Bestellprozesses in einem Online-Shop.
    • Überprüfen, ob die Login-Funktion korrekt mit Benutzerrollen und Berechtigungen funktioniert.

4. Abnahmetest

  • Beschreibung: Der Abnahmetest prüft, ob das entwickelte System die Anforderungen des Auftraggebers oder Endbenutzers erfüllt. Dabei handelt es sich um die letzte Teststufe vor der Einführung (Go-Live).
  • Ziele:
    • Überprüfung, dass alle Vereinbarungen und Anforderungen des Kunden erfüllt sind.
    • Sicherstellen, dass das System wie erwartet aus Sicht des Kunden oder Endbenutzers funktioniert.
  • Merkmale:
    • Fokus auf realistische, benutzernahe Anwendungsszenarien.
    • Typischerweise manuell durchgeführt, kann aber auch automatisiert erfolgen.
    • Umfangreiche Dokumentation und Freigabe durch den Kunden erforderlich.
  • Typische Testarten:
    • Akzeptanztests: Überprüfung von Kernanforderungen und -funktionen des Systems.
    • Benutzerakzeptanztests (UAT): Tests durch Endanwender unter realistischen Bedingungen.
  • Beispiele:
    • Der Kunde überprüft, ob ein neuer Editor in einer Textverarbeitungsanwendung gemäß den Anforderungen funktioniert.
    • Validierung, ob eine neue App die geplanten Funktionen (z. B. Push-Benachrichtigungen) erfüllt.

Die Testpyramide von Mike Cohn

Bei der Planung von Softwaretests stellt sich immer die Frage, wie viele Tests für die verschiedenen Teststufen (z.B. Komponenten-Tests, Integrationstests, Systemtests) entwickelt werden sollten, um eine effiziente Qualitätssicherung zu erreichen.

Hierfür dient die von Mike Cohn entwickelte Testpyramide als Orientierung. Sie beschreibt die optimale Verteilung von Testfällen auf die verschiedenen Testebenen, mit dem Ziel, eine effiziente, wartbare und ressourcenschonende Teststrategie zu ermöglichen.

Testpyramide von Mike Cohn

(Testpyramide von Mike Cohn)

Die Pyramide repräsentiert die Anzahl und Kosten der Testfälle für jede Ebene:

  • Je niedriger die Ebene, desto mehr Tests sollten implementiert werden, da sie kostengünstiger und schneller ausführbar sind.
  • Die höheren Ebenen sollten eine geringere Anzahl von Tests enthalten, da sie meist aufwändiger, langsamer und teurer sind.
Ebene Richtwert Anzahl Tests Verantwortlich Beschreibung
Komponententests 70–80% Entwicklungsteam Schnell, isoliert, geringer Aufwand.
Integrationstests 15–20% Entwicklungsteam (ggf. Testteam) Tests zwischen Modulen oder Schnittstellen.
Systemtests 5–10% Testteam / Entwicklungsteam Teuer, langsam, testet das gesamte System.

Hinweis: Die prozentualen Angaben sind Richtwerte und können je nach Projekt variieren.
Der Blick auf Abnahmetests, für die das Entwicklungsteam nicht verantwortlich ist, wird hier ignoriert.


Testfallkategorien

Neben der Anzahl der Tests und der Testabdeckung sind die Testfallkategorien von besonderer Bedeutung.
So sollte jeder Test zumindest

  • den “Happy Path Test”, also den idealen, fehlerfreien Ablauf, abdecken und nach Möglichkeit auch
  • Edge Case Tests enthalten, um Grenzwerte, Sonder- und Ausnahmefälle zu prüfen.
    • Die Auswahl und Anzahl hängt von Risiko, Komplexität und Kritikalität ab.

Testabdeckung

Testabdeckung misst, wie viele Codezeilen vom Testobjekt (z.B. Anforderungen, Code, Eingabebereiche) durch die Tests tatsächlich geprüft wird.
Beispiele:

  • Codeabdeckung (z.B. Anweisungs-, Zweigüberdeckung)
  • Anforderungsabdeckung
    Was ist zu beachten?
  • Es gibt keine allgemein verpflichtenden Mindestwerte.
  • Eine möglichst hohe Abdeckung ist wünschenswert, aber 100%-ige Abdeckung bedeutet nicht automatisch fehlerfreie Software.
  • Die Abdeckungsziele sollten projektspezifisch, risikobasiert und im Testplan festgelegt werden.

Testbarer Code

Qualitätsmerkmal “Testbarkeit”

ISO 25010 ist der internationale Standard zur Beschreibung von Softwarequalitätsmerkmalen und auch ein wesentlicher Bestandteil bei der Softwarearchitektur nach iSAQB (International Software Architecture Qualification Board).

Das Merkmal Testbarkeit ist dort ein Untermerkmal von Wartbarkeit (Maintainability).
Damit zeigt sich ein direkter Zusammenhang zwischen Wartbarkeit und Testbarkeit.

Im Folgenden werden die Anforderungen an wartbaren und damit auch testbarem Code herausgearbeitet.

Zentrale Anforderungen für testbaren Code

  1. Geringe Kopplung (Low Coupling):
    Klassen und Komponenten sollten möglichst wenig voneinander abhängen, damit sie isoliert getestet werden können.
  2. Hohe Kohäsion (High Cohesion):
    Eine Klasse sollte eine klar umrissene Aufgabe haben. Das erleichtert das Verständnis und das gezielte Testen.
  3. Abstraktion und Schnittstellen:
    Abhängigkeiten sollten über Schnittstellen (Interfaces) oder Abstraktionen erfolgen, nicht direkt über konkrete Implementierungen. So können beim Testen Abhängigkeiten (z.B. Datenbanken, externe Systeme) leicht durch Mocks oder Stubs ersetzt werden.
  4. Dependency Injection (Abhängigkeitsinjektion):
    Die Möglichkeit, Abhängigkeiten über Konstruktoren oder Setter einzuschleusen, anstatt sie innerhalb der Klasse selbst zu erzeugen, ist essenziell für testbaren Code.
  5. Vermeidung von statischen Abhängigkeiten:
    Statische Methoden und globale Zustände erschweren das Isolieren von Tests.
  6. Determinismus: Der Code sollte auf gleiche Eingaben immer die gleichen Ausgaben liefern und keine unerwarteten Seiteneffekte haben.
  7. Trennung von Logik und Infrastruktur: Fachliche Logik (Business Logic) sollte von technischen Details (z.B. Datenbankzugriff, Dateisystem) getrennt sein, um die Logik unabhängig testen zu können.
  8. Öffentliche Schnittstellen: Die zu testenden Methoden sollten über eine öffentliche API zugänglich sein.
  9. Keine versteckten Abhängigkeiten: Alle Abhängigkeiten sollten explizit und sichtbar sein.
  10. Feingranularität: Methoden und Klassen sollten möglichst klein und klar abgegrenzt sein, damit sie gezielt getestet werden können.

Eisbecher-Anti-Patterns

Zusammenhang zwischen Testbarkeit, Testarten und Testpyramide

Wenn zentrale Anforderungen an die Testbarkeit des Codes nicht erfüllt werden (z.B. hohe Kopplung, fehlende Schnittstellen, keine Abhängigkeitsinjektion), gestaltet sich das Schreiben isolierter Komponententests deutlich schwieriger oder aufwändiger.

In solchen Fällen wird häufig auf Integrationstests oder sogar Systemtests ausgewichen, da einzelne Komponenten nicht mehr sinnvoll isoliert getestet werden können.
Dies führt dazu, dass die Testpyramide aus dem Gleichgewicht gerät:

  • Es existieren vergleichsweise wenige (isolierte) Komponenten-Tests und ein deutlich größerer Anteil an Integrations- oder Systemtests.
    Eisbecher-Anti-Pattern

    (Eisbecher-Anti-Pattern)
  • Dieses Muster wird als “Eisbecher-Anti-Pattern” („Ice Cream Cone Anti-Pattern“) bezeichnet.

Folgen des Eisbecher-Anti-Patterns

  • Langsame Rückmeldung: System- und UI-Tests laufen deutlich langsamer als Komponenten-Tests, wodurch Fehler erst spät auffallen.
  • Erhöhte Fehleranfälligkeit: System- und UI-Tests sind oft fragil und schlagen bereits bei kleineren Änderungen fehl („Flaky Tests“).
  • Schwierige Wartbarkeit: Komplexere Tests auf hoher Ebene sind aufwändiger zu schreiben und zu pflegen.
  • Geringere Testabdeckung auf Code-Ebene: Systemtests prüfen meist nur die „Happy Paths“; Randfälle und interne Logik werden oft nicht ausreichend getestet.
  • Aufwendige Ursachenanalyse: Fehlerursachen sind in umfangreichen Systemtests oft schwer zu lokalisieren.
  • Höhere Infrastruktur- und Ausführungskosten: System- und UI-Tests benötigen aufwändige Testumgebungen und längere Laufzeiten.
  • Behinderung von Refactoring und Weiterentwicklung: Fehlende Komponenten-Tests erschweren sichere Änderungen am Code.
  • Blockierte CI/CD-Pipelines: Längere und instabile Testläufe verzögern automatisierte Build- und Deployment-Prozesse.

Umkehr des Eisbecher-Anti-Patterns

Um das Eisbecher-Anti-Pattern („Ice Cream Cone Anti-Pattern“) aufzulösen und die Testpyramide wieder in das richtige Verhältnis zu bringen, haben sich folgende Ansätze bewährt:

  1. Refactoring für bessere Testbarkeit

    • Erhöhung der Entkopplung: Entfernung oder Reduzierung direkter Abhängigkeiten zwischen Modulen und Klassen (z.B. durch Einsatz von Dependency Injection).
    • Stärkung der Kohäsion: Sicherstellung, dass Klassen und Module klar umrissene, kleine Verantwortlichkeiten haben.
    • Einführung von Schnittstellen und Abstraktionen: Austausch konkreter Implementierungen durch Schnittstellen, um Mocking/Fakes zu ermöglichen.
  2. Anpassung der Teststrategie

    • Kommunikation der Testpyramide als Zielbild: Verdeutlichung der Vorteile und Zielsetzung der Testpyramide im Team.
    • Anpassung von Definition of Done/Qualitätsrichtlinien: Festlegung von Vorgaben für einen Mindestanteil von Komponenten-Tests.
  3. Verbesserung des Test-Codes

    • Ergänzung von Komponenten-Tests: Gezieltes Schreiben von Komponenten-Tests für neu entwickelten und refaktorierten Code.
    • Schrittweise Erhöhung der Testabdeckung: Ergänzung neuer Komponenten-Tests bei jedem Refactoring oder Bugfix (Pfadfinderregel).
    • Analyse vorhandener System- und Integrationstests: Ersatz oder Aufteilung dieser Tests durch Komponenten-Tests, wo möglich.
  4. Einsatz geeigneter Werkzeuge und Techniken

    • Verwendung von Mocking-Frameworks: Erleichterung des Ersatzes von Abhängigkeiten in Komponenten-Tests.
    • Testgetriebene Entwicklung (TDD): Förderung von testbarem Code und Erhöhung der Anzahl von Komponenten-Tests.
    • Nutzung von Code-Coverage-Tools: Identifikation von Bereichen mit fehlenden Komponenten-Tests.
  5. Schulung des Teams und Veränderung der Testkultur

    • Durchführung von Test-Design-Workshops: Weiterbildung im Bereich testbarer Architektur und Test-Design.
    • Code Reviews mit Fokus auf Testbarkeit: Überprüfung, ob neuer Code sinnvoll Komponenten-testbar ist.

Zusammenfassung Ziele und Maßnahmen

Komponententests, oft als Unit-Tests bezeichnet, stellen eine wesentliche Säule der Softwarequalitätssicherung dar. Sie verfolgen das Ziel, die Funktionsweise einzelner Softwarekomponenten isoliert zu prüfen und sicherzustellen, dass sie spezifikationsgemäß arbeiten. Zu den wichtigsten Zielen gehören:

  • Frühe Fehlererkennung: Fehler werden bereits während der Entwicklung identifiziert und können schnell behoben werden.
  • Verbesserung der Codequalität: Durch das Testen der internen Logik und Kontrollstrukturen wird sauberer und wartbarer Code gefördert.
  • Regressionsschutz: Tests verhindern, dass Änderungen am Code bestehende Funktionalitäten ungewollt beeinträchtigen.
  • Unterstützung der Wartbarkeit: Eine hohe Testabdeckung erleichtert Refactorings und die Weiterentwicklung der Software.
  • Sicherstellung von Anforderungen: Komponententests garantieren, dass einzelne Module die definierten Anforderungen zuverlässig erfüllen.

Um diese Ziele zu erreichen, ist ein systematischer Ansatz erforderlich. Wichtige Maßnahmen und Wege zur Umsetzung umfassen:

  1. Definierte Testfälle: Erstellung klarer, nachvollziehbarer Testfälle basierend auf den Spezifikationen der jeweiligen Komponente.
  2. Isoliertes Testen: Verwendung von Stubs, Mocks und Fakes, um externe Abhängigkeiten zu simulieren und die Tests auf die interne Logik zu konzentrieren.
  3. Automatisierung: Automatisierte Testframeworks erlauben eine regelmäßige Ausführung der Tests, insbesondere nach Änderungen am Code.
  4. Abdeckung wichtiger Szenarien: Berücksichtigung von Randfällen, Fehlerzuständen und typischen Nutzungsszenarien.
  5. Integration in den Entwicklungsprozess: Einbindung der Tests in CI/CD-Pipelines, um die Automatisierung und kontinuierliche Qualitätssicherung zu gewährleisten.
  6. Orientierung an der Testpyramide: Sicherstellung, dass die Anzahl der Tests im Verhältnis zu den Ebenen der Testpyramide steht. Dies bedeutet, dass die meisten Tests auf der Unit-Test-Ebene stattfinden, gefolgt von einer geringeren Anzahl an Integrationstests und noch weniger Systemtests.
  7. Maßnahmen bei Eisbecher-Anti-Pattern: Falls ein Ungleichgewicht erkannt wird – z.B. überwiegend schwergewichtige Systemtests anstelle einer breiten Basis von Unit-Tests – sollten Maßnahmen wie Refactoring, Verbesserung der Teststrategie und gezielte Ergänzung fehlender Unit- und Integrationstests eingeleitet werden.

Durch die konsequente Anwendung dieser Methoden können Komponententests dazu beitragen, eine stabile und zuverlässige Software zu entwickeln, deren Qualität den Anforderungen entspricht. Gleichzeitig minimieren sie langfristig technische Schulden und erhöhen die Wartungsfähigkeit des Codes.

#Technicaldebt #Unittests