zusammenfassungen/Writerside/topics/04/Software Engineering/01_ImplementingForMaintainability.md
David Schirrmeister 4b3fdb97a3 update
2025-04-09 15:46:28 +02:00

377 lines
14 KiB
Markdown

# Implementing for Maintainability
## Motivation and Introduction
- Unit und Integration Testing ist ein bekannter Ansatz um Qualität sicherzustellen
- Gut-getestete Anwendungen:
- 1 line of code = 1-3 lines of test code
- kann auf bis zu 1:10 hochgehen
- Produkt- und Testcode werden parallel geschrieben
- guten Code schreiben ist schwer
## Black-/White-Box Testing
![image_555.png](image_555.png)
### Black-Box-Testing
> Nur das Interface ist bekannt, nicht der Inhalt
- Methode vom Software-Testing, das die Funktion analysiert, ohne den Mechanismus zu kennen
- Normalerweise rund um die Spezifikationen und Anforderungen
- _Was soll die Anwendung tun, nicht wie tut sie es_
### White-Box-Testing
> Das Interface und alle Mechanismen sind bekannt
- Testmethodik, die verifiziert, wie die Anwendung arbeitet
- Tests sind am Sourcecode ausgerichtet, nicht an den Anforderungen
### Pros / Cons
- [White-Box-Testing](01_ImplementingForMaintainability.md#white-box-testing) ist systematischer und anspruchsvoller
- Analyse des Codes kann zu Fehlerentdeckungen führen, die zuvor übersehen wurden
- Testergebnisse sind oft spröde
- Sind sehr verknüpft mit der Implementierung des Codes
- Solche Tests produzieren viele false-positives und sind nicht gut für die Metrik der Resistenz gegen Refactoring
- Können häufig nicht rückgeschlossen werden zu einem Verhalten, dass wichtig ist für eine Business-Person
- Starkes Zeichen, dass die Tests nicht viel Wert hinzufügen
- [Black-Box-Testing](01_ImplementingForMaintainability.md#black-box-testing) hat gegensätzliche Vor-/Nachteile
> Black-/White-Box-Testing sind Konzepte, die auf verschiedene Test-Typen angewendet werden können
## Testing Quadrants Matrix
![image_556.png](image_556.png)
### Quadrant 1: Technologie-fokussierte Tests, die das Development leiten
- Developer Tests:
- Unit tests
- Verifizieren Funktionalität eines kleinen Subsets des Systems
- Unit Tests sind die essenzielle Basis einer guten Test-Suite
- Verglichen mit anderen, sind sie einfach zu erstellen und warten
- Viele Unit-Tests :)
- Component-/Integration Tests:
- Verifizieren Verhalten eines größeren Teils
- Tests sind nicht für den Kunden
## Goals of Testing during Implementation
### Aktiviere nachhaltiges Wachstum des Software-Projekts
- ![image_579.png](image_579.png)
- Nachhaltigkeit ist wichtig
- Projektwachstum ist am Anfang einfach
- Das Wachstum zu halten ist schwer
- Tests können das nachhaltige Wachstum fördern
- Aber: erfordern initialen, teilweise signifikanten Einsatz
- Schlechte Tests bringen nichts
- verlangsamen am Anfang schlechten Code
- langfristig trotzdem ungünstig
- Test Code ist Teil der Codebase
- Teil, der ein spezifisches Problem behandelt - Anwendungsrichtigkeit sicherstellen
- Kosten eines Tests
- Wert
- anstehende Kosten
- _Refactoring des Tests, wenn der Code refactored wird_
- _Test bei jeder Codeänderung ausführen_
- _Mit Fehlalarmen durch den Test umgehen_
- _Zeit für das Verstehen des Tests, wenn man den zu testenden Code verstehen möchte_
### General Testing Strategy
#### Testing Automation Pyramid
![image_580.png](image_580.png)
- Software Tests in 3 Kategorien aufteilen
- **UI-Tests**
- Testen die Anwendung durch Interaktion mit der UI
- sehr high-level
- **Service Tests**
- [Black-Box](#black-box-testing) testen von größeren Softwareteilen
- _bspw. Komponenten, Services_
- **Unit Tests**
- werden während Entwicklung von Developern geschrieben
- Gibt Idee, wie viele Tests pro Kategorie in der Test-Suite sein sollten
#### Testing Ice Cream Cone
![image_581.png](image_581.png)
- Style einer Test-Suite, die häufig in der Industrie verwendet wird
- hohe Anzahl manueller, high-level Tests
- end-to-end Tests (auch an UI), die automatisch ausgeführt werden können
- nur wenige integration/unit tests
- Tests Suite mit dieser Strategie ist nicht gut wartbar
- manuelle Tests sind teuer und langwierig
- automatisierte high-level Tests gehen häufig kaputt, sobald Änderungen in der Anwendung auftreten
### Test Driven Development (TDD)
![image_582.png](image_582.png)
1. Tests schreiben
- **Design**
- Akzeptanzkriterien für den nächsten Arbeitsschritt festlegen
- Anregung [lose gekoppelte Komponenten](ImplementingForMaintainability.md#loose-coupling) zu entwerfen
- einfache Testbarkeit, dann verbinden
- Ausführbare Beschreibung von dem was der Code tut
- **Implementierung**
- Vervollständigen einer regressiven Test-Suite
2. Tests ausführen
- **Implementierung**
- Error erkennen, während der Kontext noch frisch ist
- **Design**
- Bewusstmachung, wann die Implementierung vollständig ist
> Golden Rule of TDD: schreibe niemals neue Funktionalitäten ohne einen fehlschlagenden Test
#### Vorteile des TDD
- signifikante Reduktion der Defekt-Rate
- auf Kosten eines moderaten Anstiegs im initialen Development-Prozesses
- Empirische Untersuchungen haben das noch nicht bestätigt
- TDD hat aber zu Qualitätssteigerung des Codes geführt
#### Häufige Fehler beim TDD
- Individuelle Fehler
- _Vergessen die Tests regelmäßig auszuführen_
- _Zu viele Tests auf einmal schreiben_
- _Zu große/grobe Tests schreiben_
- _zu triviale Tests schreiben, die eh funktionieren_
- Team-Fehler
- _Nur partieller Einsatz_
- _Schlechte Wartung der Test-Suite_
- _Verlassene Test-Suite (nie ausgeführt)_
## Unit-Testing vs. Integration Testing
| Unit | Integration |
|-------------------------------|----------------------|
| kleiner Teil eines Verhaltens | größere Portion Code |
![image_583.png](image_583.png)
### 4 Typen von Produktions-Code
![image_584.png](image_584.png)
#### Dimensionen
##### Komplexität und Domain-Signifikanz
- Code Komplexität
- Definiert durch Nummer der Branching-Punkte im Code
- _if-statements, Polymorphismus_
- Domain-Signifikanz
- Wie signifikant ist der Code für die problematische Domain
- normalerweise Verbindung Domain-Layer-Code zu End-User-Ziele
- Hohe Domain-Signifikanz
- _Bsp. für niedrige Relevanz: Utility Code_
##### Nummer der Kollaborateure
- Kollaborateur = Abhängigkeit, die veränderlich /& außerhalb des Prozesses ist
- veränderlich
- nicht nur read-only
- außerhalb des Prozesses
- häufig geteilt
- hindert Tests an unabhängiger Ausführung
- Code mit vielen Kollaborateuren ist schwer zu testen
#### Typen
##### Domain model & algorithms
- **Domain Code, wenige Kollaborateure**
- komplexer Code häufig Teil des Domain-Models
- wenige Kollaborateure → Testbarkeit
- sollte NIEMALS Abhängigkeiten außerhalb des Prozesses haben
##### Controllers
- **Wenig Domain Code, viele Kollaborateure**
- Koordination der Ausführung von Use-Cases für Domain-Klassen und externen Anwendungen
- Keine komplexe / Business-kritische Arbeit selbst/allein machen
- wenig Komplexität, wenig Domain-Signifikanz
- Viele Abhängigkeiten außerhalb des Projekts
##### Overcomplicated Code
- **Viel Domain Code, viele Kollaborateure**
- ist aber auch komplex und wichtig
##### Trivialer Code
- **wenig Domain-Code, wenig Kollaborateure**
#### Separierung von Controllers & DomainModel, Algorithmen
![image_585.png](image_585.png)
- Separiert komplexen Code von Code, der Orchestrierung macht
- Domain Code hat tiefe Implementierung in Business Logik
- Controller haben breite Orchestrierung aber enge Komplexität
- Erhöht Wartbarkeit und Testbarkeit
#### Wie testet man die 4 Typen?
![image_587.png](image_587.png)
- trivialer Code muss nicht getestet werden
## Unit Testing
- Beim Unit Testing gehts nicht nur um den technischen Aspekt
- möglichst wenig Zeit Input
- möglichst viel Benefits
### Gute Test-Suite
1. integriert in [SDLC](00_Introduction.md#software-development-lifecycle-sdlc)
- Tests bringen nur was, wenn man sie ständig benutzt
- am besten alle automatisiert bei jeder Änderung
2. testet nur die wichtigsten Teile der Code-Base
- Business-Logik
- Systemkritische Teile
- auch Abhängigkeiten nach außen
- Rest nur indirekt / wenig testen
3. gibt maximalen Wert mit minimalem Wartungsaufwand
- Auch automatisierte Tests müssen ggf. nach Änderungen angepasst werden
- Nur Tests behalten, die wirklich sinnvoll sind
### Was ist ein Unit-Test
> 1. Verifiziert einen kleinen Teil des Codes (unit)
>
> 2. macht es schnell
>
> 3. macht es isoliert
#### London School Interpretation
- Isolation bedeutet, dass jede Klasse ihren eigenen Test bekommt
- Auch wenn sie von gleicher Klasse erben
##### Testing Doubles
![image_591.png](image_591.png)
- Objekt, dass gleiches Verhalten und Aussehen, wie Gegenstück hat
- Vereinfachte Version, die Komplexität verringert
##### Vorteile London School Interpretation
- Wenn ein Test fehlschlägt, ist klar, was kaputt ist
- Gibt Fähigkeit den Objektgraphen aufzusplitten
- Jede Klasse hat ihre eigenen Abhängigkeiten / Vererbungen
- Schwer zu testen ohne [Testing Doubles](#testing-doubles)
- Es müssen nicht die Abhängigkeiten von Abhängigkeiten beachtet werden
- haben ja eigene Tests
- Projektweite Guideline:
- Nur eine Klasse auf einmal
- ![image_592.png](image_592.png)
#### Classic School Interpretation
- Isolation bedeutet, dass jeder Test in Isolation läuft
- Mehrere Klassen auf einmal ist okay
- Solange sie alle auf ihrem eigenen Speicher laufen
- kein geteilter Zustand
- Verhindert Kommunikation /Beeinflussung zwischen Tests
- Scheiß auf Reihenfolge oder Ergebnis von anderen Tests
##### Geteilte, private, Out-Of-Process Abhängigkeiten
- Geteilte Abhängigkeiten
- Können sich gegenseitig beeinflussen
- Müssen ersetzt werden
- _bspw. geteilte DB → neue/bearbeitete Daten können Tests beeinflussen_
- Private Abhängigkeiten → :)
- Out-Of-Process Abhängigkeiten
- Meistens ähnlich wie geteilte Abh.
- _DB = geteilt und out-of-process_
- _read-only-DB ist fine_
- also: kommt drauf an ob gut oder nicht
![image_593.png](image_593.png)
#### Beispiel Classic School
![image_594.png](image_594.png)
- _Enough inventory → purchase geht durch, inventory amount geht runter_
- _not enough product → kein purchase, keine änderungen_
- Typische AAA-Sequenz
- arrange, act, assert
- alle Abhängigkeiten und System vorbereiten
- Verhalten ausführen, das verifiziert werden soll
- Erwartete Ergebnisse überprüfen
- Outcome:
- `Customer` und `Store` werden verifiziert, nicht nur `Store`
- Jeder Bug in `Store` lässt die Tests fehlschlagen
- auch wenn `Customer` komplett richtig ist
#### Beispiel London School
![image_595.png](image_595.png)
- Gleiche Tests, aber Store wird mit test-doubles ersetzt
- "fake dependency" = "Mock"
- AAA Sequenz {id="aaa-sequenz"}
- Arrange:
- Store nicht modifizieren, stattdessen festlegen, wie er auf `hasEnoughInventory()` reagieren soll
- Act
- Assert
- Interaktion zwischen `Store` und `Customer` wird genauer untersucht
#### Vergleich Classic / London
| | Isolation of | A unit is | Uses test doubles for |
|----------------|--------------|-----------------------------|--------------------------------|
| London School | Units | A class | All but immutable dependencies |
| Classic School | Unit tests | A class or a set of classes | shared dependencies |
![image_596.png](image_596.png)
### Unit Tests - Good Practices
#### Good Practices - Structuring
![image_597.png](image_597.png)
- Offensichtliche Struktur ist wichtig
- Code wird häufiger gelesen als geschrieben
- AAA-Pattern splittet Tests in 3 Teile
- Alternative: Give-When-Then Pattern
- Einziger Unterschied: besser lesbar für nicht-Programmierer
##### Zu vermeidende Dinge
- Manchmal benutzt ein Test **mehrere AAA Steps**
- bspw: ![image_609.png](image_609.png)
- ist kein Unit-Test mehr, sondern ein [Integration-Test](#unit-testing-vs-integration-testing)
- **if-statements**
- wird schwerer lesbar
- sollte eine simple Sequenz ohne branches sein
##### Größe der [AAA](#aaa-sequenz) Sections
- **arrange**
- meistens am größten
- falls deutlich größer als act und assert zusammen
- extrahieren in neue Methode oder separate [Factory-Klasse](DesignPatterns.md#factory-method-virtual-constructor)
- **act**
- sollte nicht mehr als eine Zeile sein
- **assert**
- kann mehrere Asserts beinhalten
- eine unit kann ja auch mehrere Dinge verändern, die überprüft werden müssen
- zu viele asserts implizieren schlechten Production-Code
- _fehlende Abstraktion bspw._
- **teardown-Phase** (_alles wieder auf den alten Stand bringen_)
- die meisten unit-Tests brauchen keine
- ist meistens durch ein anderes Modell gelöst
- bspw. Mocks oder so
#### Good Practices - Naming Tests
- aussagekräftige Namen
- häufig aber schlecht:
- [MethodeDieGetestetWird]_[Szenario]_[ErwartetesErgebnis]
- Name in plain Englisch besser lesbar
- Beispiel
- _Sum_TwoNumbers_ReturnsSum()_ :(
- _Sum_of_two_numbers()_ :)
**Guideline**
- Keiner starken Benennungspolicy folgen
- Benenne die Tests als würdest du es einem nicht-Programmierer-Deppen erklären, der aber die Anwendung kennt
- Separiere Wörter, sodass sie lesbar sind
- bspw. durch `-`, `_`, `testTest`
#### Good Practices - Parameterized Tests
**Motivation:**
- Ein Test ist meistens nicht genug um eine Verhaltens-Unit zu beschreiben
- hat div. Komponenten
- Beispiel:
- Online-Store mit Lieferfunktion
- constraint: frühstes Lieferdatum ist übermorgen
- ```
public void Delivery_with_a_past_date_is_invalid()
public void Delivery_for_today_is_invalid()
public void Delivery_for_tomorrow_is_invalid()
public void The_soonest_delivery_date_is_two_days_from_now()
```
- das viel zu viel
- wenn man das mal auf größere Probleme anwendet wirds nicht besser
**Parametrisierte Tests:** {id="parametrized-tests"}
- gruppieren Tests
- meiste xUnit Frameworks haben Funktion dafür
- Beispiel in .NET:
- ![image_610.png](image_610.png)
- Also:
- weniger Test-Code
- Aber:
- schwerer herauszufinden, welche Fakten die Methode repräsentiert
- je mehr Parameter, desto schwieriger