Die Verwaltung des Zustands in Angular-Anwendungen hat sich in den letzten Jahren erheblich weiterentwickelt. Ein wichtiges Paradigma, das zunehmend in modernen Webanwendungen verwendet wird, ist die Verwendung von Signals und der Übergang zu SignalStore, insbesondere in Kombination mit NgRx. Diese Veränderungen in der Architektur bieten eine effiziente Möglichkeit, die Komplexität in großen Angular-Anwendungen zu reduzieren und gleichzeitig die Wartbarkeit und Testbarkeit des Codes zu erhöhen.

NgRx hat sich als eine der führenden Lösungen zur Handhabung von Zuständen in Angular etabliert. Es bietet eine starke Integration mit RxJS und ermöglicht eine reaktive Programmierung, bei der Zustandsänderungen auf eine deklarative Weise behandelt werden. SignalStore, eine neuere Erweiterung, führt jedoch ein neues Konzept ein, das die Handhabung von Zuständen noch flexibler und zugänglicher macht.

SignalStore basiert auf dem Signal-Mechanismus, der eine stärkere Bindung zwischen den Komponenten und dem Zustand schafft, indem er eine direktere und schnellere Möglichkeit bietet, auf Änderungen zu reagieren. Im Vergleich zu NgRx, das in seiner Struktur eher komplex ist, ist SignalStore einfach und leichtgewichtig, was besonders in großen und komplexen Projekten von Vorteil ist, da es eine klarere Trennung der Zustände ermöglicht. Der Umstieg von NgRx auf SignalStore kann je nach Anwendungsfall eine erhebliche Reduzierung der Boilerplate-Codierung bedeuten.

Die Umstellung von Observables auf Signals bringt dabei eine Reihe von Vorteilen mit sich. Insbesondere reduziert sich die Anzahl der Abonnements und Zustandsaktualisierungen, die für die Synchronisierung der Daten erforderlich sind, was den Code effizienter und verständlicher macht. Dies führt zu einer klareren, responsiveren Benutzeroberfläche, die direkt auf Änderungen reagiert, ohne die Komplexität von RxJS-Abonnements und -Pipelines zu benötigen.

Ein weiteres interessantes Konzept ist die Implementierung von Signal-Helpern, die eine noch elegantere Handhabung von Zuständen innerhalb von Komponenten ermöglichen. Diese Helfer abstrahieren die Notwendigkeit, selbst komplexe Zustandsänderungen zu verwalten, und bieten eine einfache API, um die Integration von Signals in die Anwendung zu vereinfachen. Durch den gezielten Einsatz dieser Helfer kann die Lesbarkeit und Wartbarkeit des Codes weiter optimiert werden.

In Bezug auf die Architektur ist die Verwendung von NgRx/SignalStore nicht nur eine Frage der Zustandshandhabung, sondern auch der Modularität und des Skalierens von Angular-Anwendungen. Die klare Trennung zwischen UI-Komponenten, Geschäftslogik und Datenmodell durch die Verwendung von SignalStore führt zu einer besseren Wartbarkeit und einer sauberen Architektur. Mit einem solchen Design wird es für Entwickler deutlich einfacher, neue Features zu integrieren oder bestehende zu verändern, ohne bestehende Logik zu gefährden.

Es ist jedoch wichtig, dass Entwickler nicht nur die Vorteile, sondern auch die möglichen Herausforderungen dieses Übergangs verstehen. Die Umstellung von Observables auf Signals kann zu einer signifikanten Veränderung der Denkweise und der Art und Weise, wie Anwendungen strukturiert werden, führen. Vor allem die Wechselwirkungen zwischen verschiedenen State-Management-Techniken und der Übergang von einer komplexen zu einer einfacheren Architektur erfordern ein gutes Verständnis der zugrunde liegenden Prinzipien und Best Practices.

In großen Anwendungen, in denen mehrere Teams an unterschiedlichen Aspekten des Projekts arbeiten, können SignalStore und NgRx dabei helfen, den Zustand effizient zu verwalten, ohne dass es zu einem unübersichtlichen Durcheinander von Zustandsänderungen kommt. Dies erfordert jedoch eine klare Strategie und ein durchdachtes Design, um die verschiedenen Teile der Anwendung richtig zu verbinden und Datenflüsse zu steuern.

Ein weiterer wichtiger Aspekt ist die Performance-Optimierung. NgRx und SignalStore bieten Mechanismen zur effektiven Handhabung von Zustandsaktualisierungen, die nicht nur die Entwicklungszeit verkürzen, sondern auch die Laufzeitleistung verbessern können. Die Vermeidung unnötiger Neuberechnungen und Renderings ist in komplexen Anwendungen entscheidend, um die Benutzererfahrung zu optimieren.

Neben der reaktiven Programmierung und der richtigen Verwendung von Zustandshandhabung ist es ebenso unerlässlich, auf moderne Teststrategien zu setzen. Dabei spielen Unit-Tests und End-to-End-Tests eine zentrale Rolle, da sie die Integrität und Funktionalität der Anwendung sicherstellen, insbesondere wenn umfangreiche Änderungen an der Zustandslogik vorgenommen werden. Tools wie Cypress für End-to-End-Tests und automatisierte Testframeworks ermöglichen es Entwicklern, Code-Änderungen sicher und schnell zu integrieren, ohne dass die Stabilität der Anwendung gefährdet wird.

Abschließend lässt sich sagen, dass der Übergang von klassischen State-Management-Techniken zu modernen Konzepten wie SignalStore eine Herausforderung sein kann, aber auch immense Vorteile bietet. Die Flexibilität und Performance von SignalStore gepaart mit der mächtigen reaktiven Programmierung von RxJS schaffen eine solide Grundlage für die Entwicklung hochperformanter, wartbarer und skalierbarer Angular-Anwendungen. Für Entwickler, die auf der Suche nach einer leistungsstarken Lösung für die Zustandsverwaltung in großen, komplexen Webanwendungen sind, stellt diese Kombination eine zukunftsweisende Wahl dar.

Wie man die Benutzeranmeldung und -authentifizierung in Angular effizient umsetzt

Um eine funktionale und sichere Benutzeranmeldung zu gewährleisten, müssen mehrere Schritte umgesetzt werden. Dieser Prozess umfasst das Implementieren einer Login-Funktion, das Überprüfen des Authentifizierungsstatus und das Management des Tokens. In einer typischen Angular-Anwendung lässt sich dies durch den Einsatz des AuthService und der Integration von RxJS gut realisieren.

Zu Beginn setzen wir eine einfache Login-Funktion um, die den AuthService nutzt. In der HomeComponent der Anwendung implementieren wir dazu eine login()-Methode, die die Zugangsdaten über den AuthService validiert:

typescript
import { AuthService } from '../auth/auth.service';
export class HomeComponent implements OnInit { constructor(private authService: AuthService) {} ngOnInit(): void {} login() {
this.authService.login('[email protected]', '12345678');
} }

Die Login-Funktion ruft this.authService.login() mit festen Login-Daten auf. Um den Login-Prozess reaktiv zu gestalten, können wir den Authentifizierungsstatus und den aktuellen Benutzer mit den authStatus$ und currentUser$ Observables, die vom AuthService bereitgestellt werden, überwachen. Sobald der Benutzer erfolgreich authentifiziert wurde, navigiert die Anwendung zur Route /manager.

Durch die Kombination von Observables mit dem RxJS-Operator combineLatest können wir sicherstellen, dass die Benutzeroberfläche nur dann reagiert, wenn der Login erfolgreich war. Dazu müssen wir die Login-Methode wie folgt anpassen:

typescript
import { Router } from '@angular/router'; import { combineLatest } from 'rxjs'; import { filter, tap } from 'rxjs/operators';
export class HomeComponent implements OnInit {
constructor(private authService: AuthService, private router: Router) {} login() {
this.authService.login('[email protected]', '12345678');
combineLatest([this.authService.authStatus$, this.authService.currentUser$]) .pipe( filter(([authStatus, user]) => authStatus.isAuthenticated && user?._id !== ''), tap(([authStatus, user]) => { this.router.navigate(['/manager']); }) ) .subscribe(); } }

Dies stellt sicher, dass wir nur dann weiter navigieren, wenn der Benutzer tatsächlich authentifiziert ist. Durch das Abonnieren des Streams am Ende wird der Login-Prozess aktiviert. Ein wichtiger Punkt hier ist, dass wir sicherstellen müssen, dass der Stream nur einmal aktiviert wird, um unnötige Subskriptionen zu vermeiden.

Sobald der Benutzer erfolgreich eingeloggt ist, wird das JSON Web Token (JWT) erstellt und in localStorage gespeichert. Die Entwickler-Tools von Chrome können genutzt werden, um zu überprüfen, ob das JWT korrekt in den Local Storage geschrieben wurde. Unter dem Reiter "Application" in den DevTools können wir den Token einsehen, um sicherzustellen, dass der Login-Prozess korrekt funktioniert. Falls wir die Seite neu laden, bleibt der Benutzer eingeloggt, solange der JWT nicht abgelaufen ist.

Um einen vollständigen Authentifizierungsprozess abzuschließen, müssen wir auch eine Logout-Funktion integrieren. Diese Logout-Funktion löscht das Token aus dem Local Storage und navigiert den Benutzer zurück zur Startseite:

typescript
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../../auth/auth.service'; @Component({ selector: 'app-logout', template: `Logging out...`, })
export class LogoutComponent implements OnInit {
constructor(private router: Router, private authService: AuthService) {} ngOnInit() {
this.authService.logout(true);
this.router.navigate(['/']); } }

Der entscheidende Punkt hier ist, dass wir beim Aufruf der logout()-Methode explizit true übergeben, um das JWT zu löschen. Nachdem der Logout erfolgt ist, wird der Benutzer zurück zur Startseite navigiert.

Ein weiteres wichtiges Element der Authentifizierung ist die Handhabung der Ablaufzeit des JWTs. In einem guten UX-Design ist es unpraktisch, wenn der Benutzer bei jedem Besuch der Anwendung erneut eingeloggt werden muss. Daher speichern wir das JWT in localStorage, um eine wiederholte Anmeldung zu vermeiden. Allerdings darf das Token nicht für immer gültig bleiben. Ein JWT hat eine Ablaufzeit, die vom Anbieter festgelegt wird – diese kann Minuten oder sogar Monate betragen.

Im Fall eines abgelaufenen Tokens müssen wir den Benutzer automatisch zur Login-Seite weiterleiten, um eine reibungslose Benutzererfahrung zu gewährleisten. In der AuthService-Klasse implementieren wir dazu eine Methode, die prüft, ob das Token abgelaufen ist:

typescript
protected hasExpiredToken(): boolean {
const jwt = this.getToken(); if (jwt) { const payload = decode(jwt) as any;
return Date.now() >= payload.exp * 1000;
}
return true; }

Zusätzlich kann eine weitere Methode implementiert werden, die den Authentifizierungsstatus aus dem Token extrahiert:

typescript
protected getAuthStatusFromToken(): IAuthStatus { return this.transformJwtToken(decode(this.getToken())); }

Im Konstruktor der AuthService-Klasse prüfen wir beim Laden der Anwendung, ob das gespeicherte Token abgelaufen ist. Falls ja, melden wir den Benutzer ab, andernfalls extrahieren wir den Authentifizierungsstatus und setzen ihn im AuthService.

Für den Fall, dass das Token abgelaufen ist und der Benutzer weiterhin aktiv bleibt, können wir die bestehende Logik erweitern und bei Bedarf den Benutzerstatus automatisch wiederherstellen:

typescript
protected readonly resumeCurrentUser$ = this.authStatus$.pipe(
this.getAndUpdateUserIfAuthenticated );

Dies stellt sicher, dass die Anwendung immer den aktuellen Benutzerstatus berücksichtigt und den Benutzer bei Bedarf erneut authentifiziert, ohne dass eine erneute Anmeldung erforderlich ist.


Die Implementierung eines vollständigen Authentifizierungsprozesses ist mehr als nur das Einloggen und Ausloggen eines Benutzers. Es geht darum, eine benutzerfreundliche, sichere und nahtlose Erfahrung zu schaffen. Die Behandlung von Token-Ablauf und die Sicherstellung der Authentifizierung in jeder Sitzung sind entscheidend, um die Integrität und Sicherheit der Anwendung zu wahren. Besonders wichtig ist es, den Token-Management-Mechanismus so zu gestalten, dass er die Balance zwischen Sicherheit und Benutzerfreundlichkeit wahrt. Dies erfordert eine sorgfältige Planung und Testung, um sicherzustellen, dass der Authentifizierungsworkflow auch in realen Szenarien zuverlässig funktioniert.

Warum Angular Unit-Tests keine echten Unit-Tests sind und wie man sie verbessert

In der heutigen Softwareentwicklung werden Entwickler oft mit langen, ermüdenden Code-Sitzungen konfrontiert, in denen sie auf Lösungen aus StackOverflow zurückgreifen, Code-Schnipsel aus Blogs verwenden oder sogar komplette Frameworks wie Angular in ihre Projekte integrieren. In diesem Umfeld ist es nahezu unvermeidlich, dass Fehler im Code entstehen. Wenn ambitionierte Ziele, strenge Deadlines oder unglückliche architektonische Entscheidungen hinzukommen, wird das Problem nur noch verstärkt. Automated Tests bieten eine Lösung, um sicherzustellen, dass der geschriebene Code korrekt ist und auch bleibt. In modernen Entwicklungsprozessen setzen Entwickler auf CI/CD-Pipelines, die repetitive Prozesse sicher und fehlerfrei ausführen. Doch eine Pipeline ist nur so gut wie die Qualität der darin enthaltenen Tests. Besonders im Fall von Angular stellt sich jedoch heraus, dass das, was als "Unit-Test" bezeichnet wird, in Wirklichkeit oft keiner ist.

Unit-Tests sind von zentraler Bedeutung, um sicherzustellen, dass sich das Verhalten einer Anwendung nicht unbeabsichtigt über die Zeit hinweg verändert. Sie ermöglichen es den Entwicklern, an ihrer Anwendung weiterzuarbeiten, ohne bestehende, bereits getestete Funktionalitäten zu beeinträchtigen. Ein Unit-Test ist darauf ausgelegt, nur den Code innerhalb der Funktion oder der Klasse zu testen, die im Testfall betrachtet wird. Dabei sollte es sich um schnelle, automatisierte Tests handeln, die so einfach wie möglich zu schreiben sind. Die Empfehlung ist, Unit-Tests gleichzeitig mit dem ursprünglichen Code zu schreiben, da der Entwickler andernfalls Gefahr läuft, wichtige Details oder Randfälle zu übersehen.

Es gibt jedoch eine wichtige Einschränkung: Unit-Tests sollten isoliert und unabhängig sein. Sie sollten keine Datenbankanfragen stellen, keine Netzwerkanfragen auslösen oder mit der DOM interagieren. Ein Unit-Test, der sich mit der äußeren Umgebung oder externen Abhängigkeiten beschäftigt, wird unvorhersehbar und langsamer, was dazu führt, dass er seinen ursprünglichen Zweck – nämlich schnelle und zuverlässige Tests – nicht mehr erfüllt. Hier kommen Test-Doubles ins Spiel. Sie ermöglichen es, die äußeren Abhängigkeiten zu simulieren, sodass etwa statt eines echten HttpServices ein gemockter HttpService in den Test eingebaut wird. Auf diese Weise bleibt der Test schnell und wiederholbar, ohne dass die Funktionalität der externen Komponenten wirklich getestet werden muss.

Wie viele Tests sind jedoch ausreichend? Eine gängige Faustregel besagt, dass die Menge an Testcode mindestens so groß sein sollte wie der Produktionscode. Wenn das nicht der Fall ist, besteht eine hohe Wahrscheinlichkeit, dass zu wenig getestet wird. Ein weiteres Problem bei den Tests ist, dass nicht alle Tests gleichwertig sind. Neben Unit-Tests gibt es auch Integrations- und UI-Tests. Während Unit-Tests den Fokus auf die kleinste, isolierte Einheit legen, testen Integrationstests die Zusammenarbeit mehrerer Komponenten, einschließlich Datenbankabfragen und Netzwerkanfragen. Diese Tests sind zwar gründlicher, aber auch langsamer und müssen regelmäßig gewartet werden. UI-Tests, die die Anwendung aus Sicht des Benutzers simulieren, sind oft die aufwändigsten und fragilsten Tests, da sie in der Regel nicht so zuverlässig sind wie andere Testarten.

Die Herausforderungen von Unit-Tests in Angular kommen insbesondere daher, dass eine typische Angular-Komponente aus einer Klasse und einem Template besteht. Um eine Komponente richtig zu testen, ist es notwendig, mit dem DOM zu interagieren, was den Test erheblich verlangsamt und die Komplexität erhöht. Angular-Tests müssen daher den TestBed-Mechanismus verwenden, um eine geeignete Testumgebung zu schaffen, was jedoch ebenfalls zu Performance-Einbußen führt. Das Dependency Injection System von Angular ist zudem schwerfällig und erfordert oft umfangreiche Konfigurationen, was den Testaufwand weiter steigert.

Eine Lösung für diese Probleme könnte darin bestehen, Business-Logik aus den Angular-Komponenten herauszulösen und in separate Services oder Funktionen zu verlagern. Diese können dann mit Unit-Tests isoliert getestet werden, was sowohl die Testqualität als auch die Wartbarkeit des Codes verbessert. Ein weiteres hilfreiches Tool ist Spectator, ein Framework, das es ermöglicht, Angular-Tests ohne den typischen Boilerplate-Code zu schreiben, wodurch die Tests lesbarer und einfacher zu warten sind.

Neben den traditionellen Unit-Tests gibt es auch moderne Ansätze wie Cypress-Tests, die eine bessere Möglichkeit zur Durchführung von Integrationstests bieten. Diese Tests sind nicht nur schneller und stabiler, sondern ermöglichen es auch, UI-Tests auf einer höheren Ebene zu automatisieren. Cypress bietet eine visuelle Debugging-Funktion und lässt sich problemlos in CI/CD-Pipelines integrieren, was die Effizienz des Testens weiter erhöht.

Für Angular-Anwendungen, die auf Version 17.1 und höher basieren, gibt es nun die Möglichkeit, den traditionellen Karma-Test-Runner durch den modernen Web Test Runner zu ersetzen. Dies stellt eine signifikante Verbesserung der Testgeschwindigkeit und der Flexibilität dar. Die Einrichtung des Web Test Runners ist einfach und sorgt für eine schnellere und stabilere Ausführung von Unit- und Integrationstests.

Ein entscheidender Punkt bei der Auswahl von Testmethoden in Angular ist die Realisierung, dass Unit-Tests allein nicht ausreichen, um eine umfassende Qualitätssicherung zu gewährleisten. Testen muss auf allen Ebenen der Anwendung erfolgen, und eine Kombination aus Unit-Tests, Integrationstests und UI-Tests stellt sicher, dass sowohl die interne Logik als auch die Benutzeroberfläche korrekt funktionieren. Bei der Wahl der Testmethoden sollten Entwickler auf ein ausgewogenes Verhältnis achten, bei dem Unit-Tests den Großteil ausmachen, während Integrationstests und UI-Tests nur dort zum Einsatz kommen, wo es nötig ist.