4. Mai 2023 von Frederik Schlemmer
Angular Material Karma-Tests mittels Component Harness vereinfachen
Das Problem ist nahezu jedem Frontend-Entwicklenden bekannt: Ein simpler Unit-Test wird durch die Implementierungsdetails unglaublich komplex. Dies bringt einige Nachteile mit sich, weshalb Angular seit Version 9 Component Harness zur Verfügung stellt.
Welches Problem adressieren Component Harness?
Wie bereits erwähnt, können Tests aufgrund von Implementierungsdetails sehr schnell unübersichtlich werden. Dadurch geht der Grundgedanke, dass ein Test das gewünschte Verhalten darstellen soll, verloren oder der Aufwand, den Anwendungsfall zu verstehen, steigt.
Ein Beispiel: Wir wollen eine Komponente implementieren, die einen Datepicker rendert und das ausgewählte Datum in passender Formatierung anzeigt. Der Anwendungsfall klingt trivial und wir würden erwarten, dass der Test aus ein paar Zeilen Code besteht, oder? Der Button zum Öffnen des Datepickers kann mit einer ID versehen werden und ist somit einfach im DOM auswählbar. Die Komplexität steigt jedoch enorm, wenn der Datepicker in einem Overlay dargestellt wird, wie es zum Beispiel Angular Material macht. An dieser Stelle muss sich der Test an der Implementierung der Komponente orientieren. Dadurch entsteht eine Abhängigkeit von der Implementierung der Komponente, wodurch unsere Tests bei Änderungen ebenfalls angepasst werden müssen. Dies reduziert die Wartbarkeit der Tests und erfordert eine ständige Anpassung an technische Änderungen in der Komponentenbibliothek.
Dieses Problem sowie viele Weitere werden durch Component Harness gelöst!
Was sind Component Harness?
Bevor wir uns der Lösung unserer Probleme zuwenden, ist es wichtig, die Terminologie zu erklären. Es handelt sich um eine Schnittstelle, die dem Entwickler angeboten wird, um mit der Komponente zu interagieren. Die Aufrufe dieser Schnittstelle interagieren auf die gleiche Weise wie ein Benutzer. Durch diese Zwischenschicht werden die Tests von der Implementierung der Komponente entkoppelt. Ein Component Harness ist also eine Klasse, die es dem Entwickler ermöglicht, das Benutzerverhalten über eine API abzubilden. Dadurch werden die Tests weniger anfällig für Änderungen in der Implementierung und somit wartbarer. Zudem können komplexere Funktionalitäten einer Komponente im Test abstrahiert werden.
Wie sehen unsere Unit-Tests aktuell aus?
Zunächst werfen wir einen Blick auf den aktuellen Aufbau unseres Karma-Tests. Wie bereits erwähnt, verwenden wir als Beispiel eine Datepicker-Komponente. Zunächst wollen wir nur testen, ob unsere Komponente mit dem richtigen Titel angezeigt wird. Dazu benötigen wir folgenden Testcode:
it('should have a title', () => {
init(component);
const matLabel: HTMLElement = fixture.nativeElement.querySelector('mat-label');
expect(matLabel.textContent).toBe('Archivierungsdatum');
});
Auf den ersten Blick ist hier zu erkennen, dass der Titel getestet wird. Auf den zweiten Blick wird jedoch der starke Bezug zur technischen Umsetzung der Komponente deutlich. Im nächsten Test erhöhen wir die Komplexität und testen eine Benutzereingabe. Dazu benötigen wir folgenden Testcode:
it('should have user input', () => {
init(component);
const input: HTMLInputElement = fixture.nativeElement.querySelector('mat-form-field').querySelector('input');
input.value = '2021-05-27';
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(component.getControl().value).toEqual(new Date('2021-05-27'));
});
An dieser Stelle wird deutlich, dass der Test durch seine technische Bindung an Lesbarkeit und Verständlichkeit verliert. Bei der nächsten Überarbeitung des Tests werden wir uns einige Fragen stellen:
- Warum müssen wir an dieser Stelle nach einem Mat-Form-Feld suchen und nicht nach einem Mat-Datepicker?
- Warum müssen wir ein Ereignis auslösen, nachdem wir den Wert gesetzt haben?
Das sind nur einige der Fragen. Der Test sollte nur aus drei Schritten bestehen:
- 1. die Suche nach dem Datepicker
- 2. die Eingabe des Datums durch den Benutzer
- 3. die anschließende Überprüfung des Wertes aus dem Datepicker.
Dies wird durch die Umstellung auf Component Harness erreicht.
Umstellung der Tests auf Component Harness
Die beiden vorherigen Tests werden nun auf Component Harness umgestellt. Wir beginnen wieder mit dem einfachen Testfall, der nur den Titel prüft. Durch die Umstellung erhalten wir den folgenden Testcode:
it('should have a title', async () => {
init(component);
const matFormField: MatFormFieldHarness = await loader.getHarness(MatFormFieldHarness);
expect(await matFormField.getLabel()).toEqual('Archivierungsdatum');
});
Es wird deutlich, dass in diesem Fall tatsächlich das Feld des Datepickers gesucht wird. In diesem Feld kann dann das Label überprüft werden. Dies hat den Vorteil, dass man nicht mehr wissen muss, dass der Titel als Mat-Label gerendert wird. Außerdem ist dieser Test wesentlich robuster, da er von der technischen Umsetzung entkoppelt ist. Sollte der Titel in Zukunft nicht mehr als Mat-Label gerendert werden, würde dieser Test immer noch funktionieren.
Noch deutlicher wird der Mehrwert beim zweiten Testfall. Dieser soll eine Benutzereingabe im Datepicker überprüfen. Ohne den Einsatz von Component Harness waren viele technische Vorkenntnisse und Funktionalitäten notwendig. Diese fallen durch die Umstellung komplett weg:
it('should have user input', async () => {
init(component);
const matDatePicker: MatDatepickerInputHarness = await loader.getHarness(MatDatepickerInputHarness);
await matDatePicker.setValue('2021-05-27');
expect(await matDatePicker.getValue()).toEqual('2021-05-27');
});
Die Komplexität, die durch den Testaufbau hinzukam, ist vollständig verschwunden. Bei einer Neueinführung sollten wir keine Probleme haben, den Test zu verstehen und eventuelle Anpassungen vorzunehmen. Wie müssten wir den Test anpassen, wenn es mehrere Datepicker gäbe? Wir könnten zum Beispiel den im jeweiligen Datepicker gesetzten Placeholder als Referenz verwenden.
await loader.getHarness(MatDatepickerInputHarness.with({ placeholder: 'Geburtsdatum' }));
Dadurch erhalten wir nur den Datepicker mit dem Placeholder “Geburtsdatum”.
Fazit
Nach dem Vergleich eines Karma-Tests mit und ohne Angular Component Harness werden nun die Vor- und Nachteile betrachtet. Folgende Punkte können als Vorteile genannt werden:
- Eindeutig lesbarer und wartbarer Testcode
- Vereinfachte Erstellung robuster Tests
- High-Level API zur Interaktion und Abfrage von Komponenten
- Bessere Unterstützung asynchroner Interaktionen
Letzteres ist jedoch ein größeres Thema, weshalb wir hier nicht näher darauf eingehen.
Neben den Vorteilen hat Angular Component Harness auch Nachteile:
- Zusätzliche Installation und Konfiguration
- Mögliche Verlangsamung durch die Abstraktionsschicht
- Höhere Komplexität im Vergleich zum Direktzugriff
Die letzten beiden Nachteile treten nur in seltenen Einsatzszenarien auf. Die Verlangsamung hängt von der Anzahl und der Komplexität der Tests ab. In den meisten Fällen sollte sie kaum oder gar nicht messbar sein. Außerdem sollte die Komplexität nicht steigen, wenn alle Komponenten Component-Harness unterstützen. Der Nachteil entsteht bei einer Mischung aus traditionellem Ansatz und Component Harness.
In diesem Artikel haben wir gesehen, dass Angular Component Harness ein großes Potenzial zur Vereinfachung von Testcode hat. Dennoch sollte ihr Einsatz evaluiert werden, um sicherzustellen, dass der zusätzliche Implementierungs- und Konfigurationsaufwand nicht zu hoch ist.
Weitere Informationen und Beispiele findet ihr unter Angular Material.