Wie führt die Implementierung von Geschäftslogik in Controllern zu einer schlechteren Testbarkeit?

Melden

Die Implementierung von Geschäftslogik direkt in Controllern (oft als "Fat Controller" bezeichnet) ist ein weit verbreitetes Anti-Pattern. In Bezug auf die Testbarkeit führt dies zu mehreren signifikanten Problemen:

1. Enge Kopplung an das Framework

Controller sind untrennbar mit dem Web-Framework (z. B. Spring Boot, Laravel, Express, ASP.NET) verbunden. Sie hängen von Objekten wie HttpRequest, HttpResponse, Session oder Header ab.

  • Das Problem: Wenn die Logik im Controller steckt, musst du beim Testen diese Framework-Objekte mühsam „mocken“ (simulieren).
  • Die Folge: Ein einfacher Test für eine Rabattberechnung erfordert plötzlich ein komplexes Setup, um einen HTTP-Request zu simulieren, anstatt einfach nur zwei Zahlen an eine Funktion zu übergeben.

2. Verletzung des Single Responsibility Principle (SRP)

Ein Controller sollte nur für die Steuerung des Datenflusses zuständig sein: Request entgegennehmen, Daten validieren, einen Dienst aufrufen und das Ergebnis formatieren (z. B. als JSON).

  • Das Problem: Wenn er zusätzlich Geschäftsregeln prüft oder Berechnungen durchführt, hat er mehrere Gründe, sich zu ändern.
  • Die Folge: Tests werden unübersichtlich. Ein Testfall schlägt fehl, aber du weißt nicht sofort, ob es an der Routing-Logik, der Authentifizierung oder der eigentlichen Geschäftslogik liegt.

3. Schwierigkeit von Unit-Tests (vs. Integrationstests)

Echte Unit-Tests sollten isoliert und extrem schnell sein.

  • Das Problem: Da Controller schwer vom Framework zu trennen sind, enden viele "Controller-Tests" als Integrationstests. Sie benötigen oft einen laufenden Application-Context oder einen HTTP-Server-Mock.
  • Die Folge: Die Testsuite wird langsam. Entwickler lassen langsame Tests seltener laufen, was die Feedback-Schleife verlängert und die Softwarequalität senkt.

4. Mangelnde Wiederverwendbarkeit der Logik

Geschäftslogik muss oft über verschiedene Schnittstellen zugänglich sein (z. B. Web-UI, REST-API, CLI-Befehle oder Cronjobs).

  • Das Problem: Wenn die Logik im UserController festgeschrieben ist, kann der ConsoleCommand sie nicht nutzen, ohne den Controller zu instanziieren (was meist unmöglich oder sehr schmutzig ist).
  • Die Folge: Logik wird dupliziert („Copy-Paste“). Damit müssen auch die Tests dupliziert werden. Wenn sich die Regel ändert, musst du die Tests an mehreren Stellen anpassen, was die Fehleranfälligkeit erhöht.

5. Komplexität der Test-Szenarien (Kombinatorische Explosion)

Stell dir vor, eine Geschäftsregel hat 5 Variationen und die HTTP-Schicht hat 3 mögliche Zustände (erfolgreich, nicht autorisiert, Validierungsfehler).

  • Das Problem: Wenn alles im Controller ist, müsstest du theoretisch $5 \times 3 = 15$ Tests schreiben, um alle Kombinationen im Controller-Kontext abzudecken.
  • Die Lösung durch Trennung: Du schreibst 5 reine Unit-Tests für die Logik (in einer Service-Klasse) und vielleicht 3 Tests für den Controller, um sicherzustellen, dass er die Service-Ergebnisse korrekt weitergibt. Das ist deutlich effizienter.

Zusammenfassung: Der bessere Weg

Um die Testbarkeit zu verbessern, nutzt man das Service-Layer-Pattern:

  1. Controller: Nimmt Request an, validiert Input-Format, ruft Service auf, gibt Response zurück. (Test via Integrationstests/WebMvcTest).
  2. Service (Domain Logic): Enthält die reine Geschäftslogik ohne Wissen über HTTP. (Test via schnelle, saubere Unit-Tests).

Fazit: Logik im Controller macht Tests langsam, fragil, schwer aufzusetzen und schwer zu warten. Durch die Auslagerung in reine Logik-Klassen (POJOs/Plain Objects) wird der Code modularer und lässt sich innerhalb von Millisekunden ohne Framework-Ballast testen.