Unit-Test
Unit-Tests sind automatisierte Tests, die einzelne, isolierte Codeeinheiten auf ihre korrekte Funktionsweise überprüfen. Eine solche Einheit ist typischerweise eine Funktion oder Methode – der kleinste testbare Teil einer Software. Unit-Tests bilden das Fundament der modernen Softwarequalitätssicherung und sind in der professionellen Softwareentwicklung unverzichtbar.
Der Begriff stammt aus dem Englischen: "Unit" bedeutet Einheit, "Test" bedeutet Prüfung. Im Deutschen werden Unit-Tests auch als Modultest oder Komponententest bezeichnet. Sie sind ein zentraler Bestandteil agiler Entwicklungsmethoden und ermöglichen es Entwicklern, Fehler frühzeitig zu erkennen und Code mit Zuversicht zu ändern.
Warum Unit-Tests wichtig sind
Unit-Tests lösen ein fundamentales Problem der Softwareentwicklung: Wie kannst du sicher sein, dass dein Code funktioniert? Manuelle Tests sind zeitaufwändig, fehleranfällig und nicht wiederholbar. Unit-Tests automatisieren diese Prüfung und können in Sekunden hunderte von Testfällen durchlaufen.
Die wichtigsten Vorteile von Unit-Tests sind:
- Frühe Fehlererkennung: Bugs werden sofort beim Schreiben des Codes gefunden, nicht erst in der Produktion. Je früher ein Fehler entdeckt wird, desto günstiger ist seine Behebung.
- Sicheres Refactoring: Du kannst Code umstrukturieren und verbessern, ohne Angst zu haben, etwas kaputt zu machen. Die Tests zeigen sofort, wenn sich das Verhalten ändert.
- Lebendige Dokumentation: Unit-Tests zeigen, wie eine Funktion verwendet werden soll und welches Verhalten erwartet wird. Im Gegensatz zu Kommentaren veralten Tests nicht, weil sie bei jeder Änderung ausgeführt werden.
- Besseres Design: Testbarer Code ist in der Regel auch besserer Code. Das Schreiben von Tests zwingt dich, über Schnittstellen und Abhängigkeiten nachzudenken.
Die Testpyramide
Die Testpyramide ist ein von Mike Cohn entwickeltes Konzept, das zeigt, wie verschiedene Testarten gewichtet werden sollten. Unit-Tests bilden die breite Basis dieser Pyramide.
- AWS: Was ist Unit-Testing? - Umfassende Einführung von Amazon Web Services
- JUnit 5 Dokumentation - Offizielle Dokumentation des Java-Test-Frameworks
- pytest Dokumentation - Offizielles Python-Test-Framework
- Jest Dokumentation - JavaScript-Testing mit Jest
- Martin Fowler: Test Pyramid - Grundlagen der Testpyramide
- Adesso: F.I.R.S.T.-Prinzipien - Deutschsprachiger Artikel zu Test-Prinzipien
Die Pyramidenform verdeutlicht: Du solltest viele schnelle Unit-Tests haben, weniger Integrationstests und nur wenige langsame End-to-End-Tests. Diese Verteilung sorgt für schnelles Feedback während der Entwicklung und hält die Testausführung effizient.
Das AAA-Pattern: Arrange, Act, Assert
Das AAA-Pattern (auch 3A-Muster genannt) ist der Standard für die Strukturierung von Unit-Tests. Es unterteilt jeden Test in drei klar abgegrenzte Phasen:
- Arrange (Vorbereiten): Erstelle alle benötigten Objekte, setze Testdaten und bereite den Zustand vor, der für den Test notwendig ist.
- Act (Ausführen): Führe die zu testende Aktion aus – typischerweise ein einzelner Methodenaufruf.
- Assert (Prüfen): Überprüfe, ob das Ergebnis den Erwartungen entspricht.
Hier ein Beispiel in Python mit pytest:
def test_calculate_total_price_with_discount():
# Arrange
cart = ShoppingCart()
cart.add_item("Laptop", 1000)
cart.add_item("Maus", 50)
discount = 0.10 # 10% Rabatt
# Act
total = cart.calculate_total(discount=discount)
# Assert
assert total == 945 # (1000 + 50) * 0.9
Das AAA-Pattern macht Tests lesbar und wartbar. Ein Entwickler, der den Test zum ersten Mal liest, versteht sofort, was getestet wird und welches Ergebnis erwartet wird.
Die F.I.R.S.T.-Prinzipien für gute Unit-Tests
Die F.I.R.S.T.-Prinzipien beschreiben, welche Eigenschaften gute Unit-Tests haben sollten:
- Fast (Schnell): Unit-Tests müssen schnell sein – idealerweise nur wenige Millisekunden pro Test. Langsame Tests werden seltener ausgeführt und verzögern das Feedback.
- Independent (Unabhängig): Tests dürfen nicht voneinander abhängen. Jeder Test muss in beliebiger Reihenfolge ausführbar sein und sein eigenes Setup mitbringen.
- Repeatable (Wiederholbar): Tests müssen bei jedem Durchlauf das gleiche Ergebnis liefern – unabhängig von Uhrzeit, Umgebung oder vorherigen Testläufen.
- Self-Validating (Selbstprüfend): Ein Test liefert ein klares Ergebnis: bestanden oder fehlgeschlagen. Kein manuelles Prüfen von Logdateien oder Konsolenausgaben.
- Timely (Zeitnah): Tests sollten zeitnah zum Code geschrieben werden, idealerweise vor oder parallel zur Implementierung (siehe Test-Driven Development).
Mocking: Abhängigkeiten isolieren
Ein zentrales Konzept beim Unit-Testing ist die Isolation. Die getestete Einheit soll unabhängig von ihren Abhängigkeiten getestet werden. Wenn eine Funktion zum Beispiel Daten aus einer Datenbank liest, willst du nicht bei jedem Test eine echte Datenbankverbindung aufbauen. Hier kommen Test Doubles ins Spiel:
| Typ | Beschreibung | Beispiel |
|---|---|---|
| Mock | Simuliert Objekte und prüft, ob bestimmte Methoden aufgerufen wurden | Prüfen, ob eine E-Mail-Funktion aufgerufen wurde |
| Stub | Gibt vordefinierte Werte zurück | Repository gibt immer den gleichen Testdatensatz zurück |
| Fake | Vereinfachte, aber funktionierende Implementierung | In-Memory-Datenbank statt echter Datenbank |
| Dummy | Platzhalter, der nie verwendet wird | Pflichtparameter, der im Test nicht relevant ist |
Ein Beispiel mit Mocking in Java mit Mockito:
@Test
void testOrderService_sendsEmailAfterOrder() {
// Arrange
EmailService emailMock = mock(EmailService.class);
OrderService orderService = new OrderService(emailMock);
Order order = new Order("Laptop", 1);
// Act
orderService.placeOrder(order);
// Assert
verify(emailMock).sendConfirmation(order);
}
Beliebte Test-Frameworks
Für jede Programmiersprache gibt es etablierte Test-Frameworks, die das Schreiben und Ausführen von Unit-Tests erleichtern:
- Java: JUnit ist das Standard-Framework, oft kombiniert mit Mockito für Mocking. JUnit 5 bietet moderne Features wie parametrisierte Tests und Nested-Testklassen.
- Python: pytest ist das beliebteste Framework mit einfacher Syntax und leistungsstarken Fixtures. Alternativ gibt es das eingebaute unittest-Modul.
- JavaScript: Jest von Meta bietet Zero-Configuration, Snapshot-Testing und integriertes Mocking. Alternativen sind Mocha und Vitest.
- PHP: PHPUnit ist der Standard für PHP-Entwicklung und folgt dem xUnit-Muster.
- C#: MSTest, NUnit und xUnit.net sind die drei großen Frameworks für .NET-Entwicklung.
Testabdeckung messen
Die Testabdeckung (Code Coverage) misst, welcher Anteil des Quellcodes durch Tests ausgeführt wird. Sie wird als Prozentsatz angegeben und hilft einzuschätzen, wie gut dein Code getestet ist.
Es gibt verschiedene Arten der Abdeckungsmessung:
- Zeilenabdeckung: Wie viele Codezeilen wurden ausgeführt?
- Branch-Abdeckung: Wurden alle Verzweigungen (if/else) getestet?
- Funktionsabdeckung: Wurden alle Funktionen aufgerufen?
Eine Testabdeckung von 70-80% gilt als guter Richtwert. Höhere Werte anzustreben ist oft unverhältnismäßig aufwändig. Wichtiger als die Zahl ist die Qualität der Tests: Eine hohe Abdeckung nützt nichts, wenn die Tests keine sinnvollen Assertions enthalten.
Beliebte Tools zur Messung der Testabdeckung sind JaCoCo (Java), coverage.py (Python), Istanbul/nyc (JavaScript) und PHPUnit (PHP, integriert).
Test-Driven Development
Test-Driven Development (TDD) ist eine Entwicklungsmethode, bei der Tests vor dem produktiven Code geschrieben werden. Der Ablauf folgt dem Red-Green-Refactor-Zyklus:
- Red: Schreibe einen Test für die gewünschte Funktionalität. Der Test schlägt fehl, weil der Code noch nicht existiert.
- Green: Schreibe den minimalen Code, der nötig ist, damit der Test besteht.
- Refactor: Verbessere den Code, ohne das Verhalten zu ändern. Die Tests stellen sicher, dass nichts kaputtgeht.
TDD führt zu hoher Testabdeckung, weil jede Zeile Code geschrieben wird, um einen bestehenden Test zu erfüllen. Außerdem entstehen dadurch automatisch gut strukturierte, testbare Architekturen.
Unit-Tests in der Praxis
Unit-Tests sind ein essentielles Werkzeug für alle Fachinformatiker für Anwendungsentwicklung. In Bewerbungsgesprächen und in der Abschlussprüfung werden häufig Fragen zu Testmethoden gestellt. Auch im Berufsalltag gehört das Schreiben von Tests zur täglichen Arbeit.
In modernen Entwicklungsteams werden Unit-Tests automatisch in CI/CD-Pipelines ausgeführt. Bei jedem Commit prüft das System, ob alle Tests bestehen. Code mit fehlgeschlagenen Tests wird nicht in den Hauptbranch gemergt. Dies stellt sicher, dass die Codebasis stabil bleibt.