Testen ist ein integraler Bestandteil unserer Arbeit und die wenigsten unserer Projekte kommen ohne automatische Unit- oder Integrationstests aus. Sie helfen, unsere Qualitätsstandards einzuhalten, stärken das Vertrauen in die Software und schützen vor ungewollten Seiteneffekten einer geplanten oder ungeplanten Änderung.
Möchten wir grafische Oberflächen – zum Beispiel eine Website oder eine Angular-Komponente – testen, haben wir es früher oder später mit Benutzereingaben zu tun. In diesem Artikel zeige ich, wie Tastatureingaben oder Paste-Events in Integrationstests simuliert werden können.
Beispielanwendung
Meine Erfahrung zeigt, dass man am besten an praktischen Beispielen lernt. Daher habe ich für diesen Artikel eine kleine Anwendung entwickelt. Die Funktionsweise ist schnell beschrieben: Man kann Einträge zu einer Aufgabenliste hinzufügen, indem etwas in ein Textfeld eingegeben und mit der Enter-Taste bestätigt wird. Optional ist es möglich, einen Text aus der Zwischenablage einzufügen. In beiden Fällen wird die Liste so lange erweitert, bis ein festgelegtes Limit erreicht ist. Danach können keine weiteren Aufgaben hinzugefügt werden.
Wie der Titel dieses Beitrages verrät, ist unsere TaskApp eine Angular Anwendung. Angular ist eine Entwicklungsplattform zum Erstellen effizienter und anspruchsvoller Applikationen auf Basis von TypeScript und ist mit allem ausgestattet, was zum Entwickeln und Testen notwendig ist. Gerade im Enterprise-Umfeld kann das Framework seine Stärken ausspielen und so ist es nachvollziehbar, dass unsere Partner auf Angular setzen: SAP mit Spartacus, der Storefront der SAP Commerce Cloud und CELUM mit der Nova UI, der neuen Benutzeroberfläche des ContentHubs.
Praxistipp: Arrange, Act, Assert
Wir versuchen, wo immer es sinnvoll ist, dem AAA-Pattern zu folgen. Dieses Pattern beschreibt, dass ein Test in drei logische Abschnitte unterteilt werden sollte:
- Arrange,
- Act,
- Assert.
Jeder dieser Bereiche ist nur für den Teil verantwortlich, nach dem er benannt ist. Dieses Muster hilft, Tests besser zu strukturieren und verständlich zu gestalten.
Tests
Zunächst definieren wir unsere Testfälle. Sie werden durch die Funktionsweise unserer Anwendung vorgegeben und stellen die grundsätzliche Funktionalität sicher.
describe('Useless TaskApp', () => {
it('should add a task after pressing enter');
it('should add a task using paste only if enabled');
it('should update the task headline correctly');
it('should ignore whitespace only tasks');
it('should not add a task after the task limit was reached');
it('should disable the textbox after the limit was reached');
it('should update the task hint properly');
it('should clear the textbox after a task was added using enter-key');
it('should clear the textbox after a task was added using paste');
});
Zur Erinnerung: Wir möchten sicherstellen, dass ein in eine Textbox eingegebener Text nach dem Drücken der Enter-Taste in der Aufgabenliste erscheint. Gehen wir den ersten Testfall einmal Beispielhaft durch.
- Finde das Textbox-Element
- Füge einen Text in das Textbox-Element ein
- Drücke Enter (simuliert)
- Prüfe ob die Aufgabe mit dem eingegebenen Text in der Liste erscheint
Um DOM-Elemente effektiv und zielgenau zu selektieren, verwenden wir data-*
-Attribute. So können Selektoren unabhängig von CSS-Klassen und IDs gewählt werden, denn die werden in modernen Anwendungen häufig dynamisch generiert. Zusätzlich erreichen wir so eine gewisse Unabhängigkeit gegenüber dem Entwicklungsprozess, denn Anpassungen an den Tests werden bei Änderungen am Anwendungscode weitestgehend vermieden.
Beispiel
it('should add a task after pressing enter', () => {
// Arrange
const tasktext = 'Make appointment';
const taskbox = findElement(fixture, '[data-test-taskbox]'); // 1.
const input = taskbox as HTMLInputElement;
// Act
input.value = tasktext; // 2.
taskbox.triggerEventHandler('keydown.enter', {target: input}); // 3.
fixture.detectChanges();
// Assert
const tasklist = findElement(fixture, '[data-test-tasklist]');
expect(tasklist.nativeElement.textContent).toEqual(tasktext); // 4.
});
Die Funktion findElement()
ist kein Bestandteil der Angular Testing-Tools sondern eine kleine Hilfsfunktion um Elemente zu selektieren. Sie gibt eine Angular DebugElement-Instanz zurück, die eine praktische Methode zum Auslösen von Ereignissen bereitstellt: triggerEventHandler()
.
triggerEventHandler()
Wird ein Listener an ein Element gebunden, kann das Ereignis anhand seines Namens ausgelöst werden. Das funktioniert aber nur, solange wir das Angular-eigene Event-Binding verwenden.
Spannend sind in diesem Zusammenhang sind die sogenannten pseudo-events für Tastatureingaben. Sie werden in der Angular-Dokumentation zwar erwähnt, aber nicht näher beschrieben. Grob gesagt sind pseudo-events eine Abstraktionsschicht um KeyboardEvent-Objekte und erlauben es, direkt auf einen bestimmten Tastendruck oder eine Tastenkombination zu reagieren. Das bedeutet, dass ein Ereignis nur bei genau dieser Taste bzw. Tastenkombination ausgelöst wird und nicht bei jedem Tastendruck überprüft werden muss, ob die gerade gedrückte Taste der Erwartung entspricht. Wer Interesse daran hat, kann sich den Quellcode bzw. die zugehörigen Tests durchlesen.
Eine Hilfsfunktion, die triggerEventHandler implementiert könnte so aussehen:
function addByEnter(fixture: ComponentFixture<unknown>, text: string: void {
const element = findElement(fixture, '[data-test-taskbox]'); // 1.
const input = element.nativeElement as HTMLInputElement; // 2.
input.value = text;
element.triggerEventHandler('keydown.enter', { target: input }); // 3.
fixture.detectChanges();
}
- Finde das Element mit dem Attribut
data-test-taskbox
- Hole das DOM-Element
- Simuliere den Tastendruck. Wichtig ist hier das Ziel im eventObjekt korrekt anzugeben – in diesem Fall das HTMLInputElement
Native APIs
Für Fälle, in denen mit Events, die Angular unbekannt sind, umgegangen werden muss, stehen uns die nativen APIs zur Verfügung. Im folgenden Beispiel erzeugen wir ein Paste-Event und lösen es aus.
function addByPaste(fixture: ComponentFixture<unknown>, text: string): void {
const element = findElement(fixture, '[data-test-taskbox]');
const input = element.nativeElement as HTMLInputElement;
const clipboardEvent: Event = new Event('paste', {});
clipboardEvent.clipboardData = {
getData: () => text
};
input.dispatchEvent(clipboardEvent);
fixture.detectChanges();
}
Benutzereingaben in Integrations- und Komponententest zu simulieren, ist nicht schwer. Man muss nur die richtigen Werkzeuge kennen. Happy Testing!
Der Sourcecode kann auf Github heruntergeladen oder auf Stackblitz direkt ausprobiert werden.