25 KiB
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
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 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 hat gegensätzliche Vor-/Nachteile
Black-/White-Box-Testing sind Konzepte, die auf verschiedene Test-Typen angewendet werden können
Testing Quadrants Matrix
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
- Unit tests
-
Tests sind nicht für den Kunden
Goals of Testing during Implementation
Aktiviere nachhaltiges Wachstum des Software-Projekts
- 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
- Software Tests in 3 Kategorien aufteilen
- UI-Tests
- Testen die Anwendung durch Interaktion mit der UI
- sehr high-level
- Service Tests
- Black-Box testen von größeren Softwareteilen
- bspw. Komponenten, Services
- Black-Box testen von größeren Softwareteilen
- Unit Tests
- werden während Entwicklung von Developern geschrieben
- UI-Tests
- Gibt Idee, wie viele Tests pro Kategorie in der Test-Suite sein sollten
Testing Ice Cream Cone
- 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)
- Tests schreiben
- Design
- Akzeptanzkriterien für den nächsten Arbeitsschritt festlegen
- Anregung lose gekoppelte Komponenten zu entwerfen
- einfache Testbarkeit, dann verbinden
- Ausführbare Beschreibung von dem was der Code tut
- Implementierung
- Vervollständigen einer regressiven Test-Suite
- Design
- Tests ausführen
- Implementierung
- Error erkennen, während der Kontext noch frisch ist
- Design
- Bewusstmachung, wann die Implementierung vollständig ist
- Implementierung
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 |
4 Typen von Produktions-Code
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
- veränderlich
- 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
- 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?
- 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
- integriert in SDLC
- Tests bringen nur was, wenn man sie ständig benutzt
- am besten alle automatisiert bei jeder Änderung
- Tests bringen nur was, wenn man sie ständig benutzt
- testet nur die wichtigsten Teile der Code-Base
- Business-Logik
- Systemkritische Teile
- auch Abhängigkeiten nach außen
- Rest nur indirekt / wenig testen
- gibt maximalen Wert mit minimalem Wartungsaufwand
- Auch automatisierte Tests müssen ggf. nach Änderungen angepasst werden
- Nur Tests behalten, die wirklich sinnvoll sind
- Auch automatisierte Tests müssen ggf. nach Änderungen angepasst werden
Was ist ein Unit-Test
Verifiziert einen kleinen Teil des Codes (unit)
macht es schnell
macht es isoliert
London School Interpretation
- Isolation bedeutet, dass jede Klasse ihren eigenen Test bekommt
- Auch wenn sie von gleicher Klasse erben
Testing Doubles
- 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
- Es müssen nicht die Abhängigkeiten von Abhängigkeiten beachtet werden
- haben ja eigene Tests
- Projektweite Guideline:
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
- Mehrere Klassen auf einmal ist okay
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
- Können sich gegenseitig beeinflussen
- Private Abhängigkeiten → :)
- Out-Of-Process Abhängigkeiten
Beispiel Classic School
-
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
- arrange, act, assert
-
Outcome:
Customer
undStore
werden verifiziert, nicht nurStore
- Jeder Bug in
Store
lässt die Tests fehlschlagen- auch wenn
Customer
komplett richtig ist
- auch wenn
Beispiel London School
-
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
- Store nicht modifizieren, stattdessen festlegen, wie er auf
- Act
- Assert
- Interaktion zwischen
Store
undCustomer
wird genauer untersucht
- Interaktion zwischen
- Arrange:
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 |
Unit Tests - Good Practices
Good Practices - Structuring
- 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
- Alternative: Give-When-Then Pattern
Zu vermeidende Dinge
- Manchmal benutzt ein Test mehrere AAA Steps
- bspw:
- ist kein Unit-Test mehr, sondern ein Integration-Test
- bspw:
- if-statements
- wird schwerer lesbar
- sollte eine simple Sequenz ohne branches sein
Größe der AAA Sections
- arrange
- meistens am größten
- falls deutlich größer als act und assert zusammen
- extrahieren in neue Methode oder separate Factory-Klasse
- 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.
- kann mehrere Asserts beinhalten
- 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
- bspw. durch
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
-
- constraint: frühstes Lieferdatum ist übermorgen
- Online-Store mit Lieferfunktion
Parametrisierte Tests: {id="parametrized-tests"}
- gruppieren Tests
- meiste xUnit Frameworks haben Funktion dafür
- Beispiel in .NET:
- Also:
- weniger Test-Code
- Aber:
- schwerer herauszufinden, welche Fakten die Methode repräsentiert
- je mehr Parameter, desto schwieriger
Integration Testing
-
Verifiziert größeren Teil des Systems (mehrere Units)
- aus dem Controllers-Quadranten
-
braucht evtl. länger als ein Unit-Test
-
ist evtl. abhängig von anderen Tests (keine Isolation)
-
Balance zwischen Unit- und Integration-Tests ist wichtig
- direktes Arbeiten mit out-of-process-Abhängigkeiten macht Integration Tests langsam
- sind teuer zu warten
- dafür besserer Schutz gegen Zurückentwicklungen
- Integration-Tests für den längsten Happy-Path mit den meisten Abhängigkeiten
- Eck-Szenarien des Business-Szenarios mit Unit-Tests
- direktes Arbeiten mit out-of-process-Abhängigkeiten macht Integration Tests langsam
-
Out-of-Process Abhängigkeiten
- Managed (Abhängigkeiten über die wir volle Kontrolle haben)
- nur Zugriff über die Applikation
- Interaktionen sind für die externe Welt nicht sichtbar
- nicht mocken
- wir wollen, dass die tests fehlschlagen, wenn wir was an der DB ändern bspw.
- Unmanaged (Abhängigkeiten über die wir keine Kontrolle haben)
- mocken
- wir wollen volle Kontrolle über die Tests
- mocken
- Managed (Abhängigkeiten über die wir volle Kontrolle haben)
Integration Testing - Example
- alle User sind in einer DB
- Änderungen werden an angeschlossene Anwendungen über Message-Bus geschickt
- Business-Rules
- Falls Email zur Company-Domain gehört → Employer Status, sonst Customer
- System muss Anzahl der Employer tracken (auch beim Wechseln)
- Wenn Mail wechselt müssen externe Systeme über MessageBus benachrichtigt werden
-
Domain model, Algorithms
- Company
- Attribute:
DomainName
NumberOfEpmoyees
- Methoden:
ChangeNumberOfEmployees(...)
IsEmailCorporate(...)
- Attribute:
- CompanyFactory
- Erstellt
Company
-Objekte anhand von DB-Feldern
- Erstellt
- User
- Attribute:
UserId
Email
UserType
EmailChangedEvents[]
- Methoden:
ChangeEmail(...)
- Attribute:
- UserFactory
- Erstellt
User
-Objekte anhand von DB-Feldern
- Erstellt
- Company
-
Controllers
- UserController
- Delegiert die Ausführung von Email-Änderungen
- Logik ist gekapselt in den Domain-Klassen
- Managed Kommunikation mit out-of-process-Abhängigkeiten
- hier: DB und messageBus
- UserController
-
Integration Test für den längsten Happy-Path
- geht durch alle Abhängigkeiten
- hier: corporate → non-corporate Mail
- davor: out-of-process-Abhängigkeiten Kategorisieren
- was direkt testen, was mocken?
- hier: DB ist managed → direkt testen
- hier: messageBus ist unmanaged → mocken
- OCP anwenden durch ein Interface
- was direkt testen, was mocken?
- geht durch alle Abhängigkeiten
-
Edge-cases mit Unit-Tests
Integration Testing - Good Practices
- Immer einen festen Platz für das Domain-Model in der Code-Base haben
- unit-tests dafür, integration-tests für Controller
- Abgrenzung kann unterschiedlich vorgenommen werden
- package, namespace, assembly, ...
- Möglichst wenig Layer in der Anwendung
- Dopplungen im AAA-Schema
Test Coverage
Test-Abdeckung wird häufig genutzt, um die Qualität einer Test-Suite zu beurteilen
Niedrige Coverage zeigt, dass die Test-Suite nicht ausreichend ist.
Hohe Coverage bedeutet nicht, dass die Test-Suite gut ist
Code Coverage
- Anteil der Code-Zeilen, die durch die Tests ausgeführt werden
- Beispiel:
Branch Coverage
- Anteil der Code-Branches die durch die Tests ausgeführt werden
- Beispiel:
- Gibt meist wertvollere Ergebnisse als Code-Coverage
Probleme mit Coverage-Metriken
- Keine Garantie, dass der Test alle möglichen Ausgänge überprüft
- Keine Coverage-Metrik kann Code in externen Libraries abbilden
- Externe Bibliotheken
- Coverage-Metriken sind nur Indikatoren, nix tatsächlich final festlegendes
Rubber Duck Debugging
Wenn verzweifelt → Den Fehler einer Gummiente erklären
- Wenn man versucht jemandem das Problem zu erklären ist man gezwingen
- Das Problem von einer anderen Perspektive anzusehen
- Dadurch ein tieferes Verständnis vom Problem zu bekommen
- einfacher eine Lösung zu finden
Pair Programming
- Zu zweit auf einer Maschine Code schreiben
- gleichzeitig auch Arbeit planen und diskutieren
- Idee dahinter:
- zwei Gehirne und vier Augen sind besser als ein Gehirn und 2 Augen
- Teams haben ausprobiert und festgestellt
- geht schneller, da man fokussierter bleibt
- höhere Code-Qualität
Pairing Styles
Pairing Style: Driver and Navigator
- Fahrer
- Person an der Tastatur
- ist fokussiert das nächste kleine Ziel zu erreichen
- ignoriert größere Probleme
- sollte die ganze Zeit mitreden, was er tut
- Navigator (hoffentlich ohne link rechts Schwäche)
- ist in der überwachenden Position, während der Fahrer tippt
- Reviewed den Code durchgängig
- direktes Feedback
- Hat auch größere Probleme, Bugs im Kopf
- Notizen für mögliche nächste Schritte
- Möglicher Arbeitsablauf:
Pairing Style - PingPong
- Perfekt für einen TDD-Task
- Ping
- Dev A schreibt einen fehlschlagenden Test
- Pong
- Dev B schreibt Implementierung damit er klappt
- Ping
- Dev B schreibt den nächsten Test
- Ping
- Bei jedem Pong kann man auch nochmal refactoren
Pairing Style - Strong-Style Pairing
- Super für Wissenstransfer
- Regel: "Jede Idee, die von deinem Kopf in den Computer soll, muss durch die Hände von jemand anderem gehen"
- Navigator
- erfahrene Person
- Fahrer
- Person die was lernen will/soll
- sollte dem Navigator voll vertrauen
- Warum?-Fragen und Challenges nach dem Implementieren besprechen
Pairing Style - Research and Explore
- Überlegen und herausfinden ist häufig notwendig
- bspw. bei neuen Technologien etc.
- Wie kann man das im Pair-Modus angehen
- Liste mit Fragen für eine mögliche Situation machen
- Aufsplitten für einzelne Zeitslots
- entweder Fragen aufteilen oder gleichzeitig Antworten auf gleiche suchen
- Zusammenkommen und diskutieren/teilen was man gefunden hat
Pairing Style - Documentation
- Je nach Situation und Präferenz
- Dokumentation zusammen machen
- Einer schreibt, anderer macht Anmerkungen
Benefits / Challenges
Static Code Analysis
- Code analysieren, ohne ihn auszuführen
- Tools inspizieren das Programm für ...
- alle möglichen Verhalten
- suchen Coding-Fehler, Back-Doors, ...
- generieren Metriken für den Code die helfen die Qualität zu analysieren
- Kann Qualität verbessern
Beispiele für die Identifikation von Bekannten Problemen und Bad Practices
- Generische Beispiele
- Größe
- Kann verschieden gemessen werden
- Code-Zeilen, Klassen, Dateien, Funktionen, ...
- Kann verschieden gemessen werden
- Kommentare
- zeigen häufig, dass der Code zu komplex ist
- Duplikate
- Dont repeat yourself!
- Größe
- Sicherheitsrelevante Issues
- OWASP Top Ten
- Sammlung von kritischen Risiken für Anwendungssicherheit
- SAST Tools
- finden bekannte Risiken
- OWASP Top Ten
Simple Metrik für Komplexität: McCabe Metrik
- Cyclomatische Komplexität
- Behandelt Programmstuktur als Graphen
- Wird folgendermaßen kalkuliert:
-
C = E-N + 2P
- E = Nummer der Ecken
- N = Nummer der Nodes
- P = Nummer der verbundenen Komponenten (für eine OO Funktion ist P = 1)
-
- Beispiel:
-
C = 9-8+ (2*1) = 3
-
Übung Test Cases, Dependencies and more
-
:
- BriefSystem - Controller
- BriefErstellung - Domain
- Empfaenger - trivialer Code
- EmpfaengerInterface - Domain Model
- EmpfaengerCsvLeser - trivialer Code
- Brief - trivialer Code
-
: alle
-
:
- wurdeEmpfängerHinzugefügt
- Empfänger über Interface hinzufügen
- Brief erstellen und schauen ob Empfänger hinzufügbar ist
- EmpfängerListe kann sonst nicht abgefragt werden
- :