# 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 ## Integration Testing - Verifiziert größeren Teil des Systems (mehrere Units) - aus dem [Controllers-Quadranten](#4-typen-von-produktions-code) - 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 - 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 ### Integration Testing - Example **Customer Management System** ![image_611.png](image_611.png) - 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 ![image_612.png](image_612.png) - **Domain model, Algorithms** - Company - Attribute: `DomainName` `NumberOfEpmoyees` - Methoden: `ChangeNumberOfEmployees(...)` `IsEmailCorporate(...)` - CompanyFactory - Erstellt `Company`-Objekte anhand von DB-Feldern - User - Attribute: `UserId` `Email` `UserType` `EmailChangedEvents[]` - Methoden: `ChangeEmail(...)` - UserFactory - Erstellt `User`-Objekte anhand von DB-Feldern - **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 - **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](DesignPrinciples.md#o-open-closed-principle-ocp) anwenden durch ein Interface_ - ![image_614.png](image_614.png) - **Edge-cases mit Unit-Tests** ![image_613.png](image_613.png) ### Integration Testing - Good Practices - Immer einen festen Platz für das [Domain-Model](#domain-model-algorithms) 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 - Abstraktionen und Generalisierungen → mehr Layer - wird schwer den Code nachzuvollziehen - ![image_615.png](image_615.png) - ![image_616.png](image_616.png) - Dopplungen im [AAA](#aaa-sequenz)-Schema - teilweise verlockend - ![image_617.png](image_617.png) - Falls einer nicht funktioniert geht der andere nicht - Ausnahme: - out-of-process-Abhängigkeit, die nur schwer in den gewünschten Zustand kommt