Zwischen Erwartung und Ergebnis: End-to-End-Tests für Web-Frontends

Für die Qualitätssicherung von Frontend-Anwendungen können End-to-End-Tests bessere Dienste leisten als herkömmliche Methoden.

Lesezeit: 6 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 2 Beiträge
Von
  • Oliver Zeigermann
Inhaltsverzeichnis

End-to-End-Tests (E2E) waren bisher vor allem für ihre Unzuverlässigkeit berühmt und berüchtigt. Das ändert sich zumindest teilweise mit modernen Testframeworks. Daher sollten E2E-Tests aus Sicht des Autors nicht die Krone auf der Test-Pyramide von Frontends sein, sondern der zentrale Ausgangspunkt. Dabei sollten Teams nur für die Bereiche, die sich nicht sinnvoll mit E2E-Tests prüfen lassen, manuell testen oder mit Unit-Tests arbeiten. Das gilt für Frontends, während für Backends nach wie vor etablierte Testing-Ansätze besser geeignet sind.

Ein End-to-End-Test ist eine spezielle Form eines Integrationstests, bei dem die komplette Anwendung gestartet und das Zusammenspiel von Frontend und Backend getestet wird. Ein Testskript klickt die Anwendung in derselben Weise durch wie ein menschlicher Benutzer. Dabei prüft der Test, ob die Anwendung die erwarteten Ergebnisse liefert und tatsächlich alle Teile der Anwendung erreichbar sind. Außerdem testet er, ob die Anwendung in allen gewünschten Browsern läuft. Zentrales Werkzeug für das folgende Beispielszenario ist TestCafe, aber die gezeigten Ansätze lassen sich weitgehend auf andere E2E-Test-Frameworks übertragen.

Die grauen Bereiche in der Beispielanwendung sind klickbar und links unten ist ein Eingabefeld (Abb. 1).

Die grauen Bereiche in der Beispielanwendung sind klickbar und links unten ist ein Eingabefeld (Abb. 1).

Das Tool TestCafe erlaubt die Spezifikation eines Tests als gültigen JavaScript- oder TypeScript-Code. Mit fixture legen Entwickler die Testsuite fest, und der erste Parameter der Funktion gibt den Namen des Tests an. Verarbeitet wird asynchron, da TestCafe automatisch wartet, bis die ausgeführte Aktion beendet ist. Wahllos verstreute Wait-Anweisungen gehören damit der Vergangenheit an.

Folgendes Beispiel prüft zunächst den angezeigten Wert auf 0, drückt anschließend den Plus-Button und überprüft am Ende, ob der angezeigte Wert 1 ist. Dabei gilt die Annahme und Hoffnung, dass nach einem erfolgreichen Test Menschen die Anwendung auf dieselbe Art bedienen können. Ein solcher Test ist damit nah an der realistischen Anwendung.

fixture 'App Browser Test'
    .page 'http://localhost:3000';

test('increase', async t => {
    await t.expect(valueEl.innerText).eql('0');
    await t.click(incrementBtn);
    await t.expect(valueEl.innerText).eql('1');

});

Im Code fehlen noch die sogenannten Selektoren für die zu testenden Elemente, valueEl und incrementBtn. Sie geben an, auf welche Teile der Anwendung sich die Testanweisungen beziehen. Für die Umsetzung existieren unterschiedliche Stile. Die klassische und robuste Variante funktioniert über zusätzliche Test-IDs, die Anwendungsentwickler den DOM-Elementen für genau diesen Testzweck hinzufügen. Der zweite Ansatz nutzt die auch für Endnutzer verfügbaren Attribute der Web Accessibility Initiative (WAI) im Interesse eines barrierefreien Zugangs zu Webinhalten. Die Selektoren greifen dabei auf erklärende Labels und Rollen aus dem DOM zu. TestCafe erlaubt an dieser Stelle beliebige CSS-Selektoren.

Variante drei ist komplett auf dieser Philosophie aufgebaut: Die schlicht Testing Library genannte Bibliothek folgt dem Motto "Je mehr die Tests der Art ähneln, wie die Software genutzt wird, desto mehr Vertrauen schaffen sie". Sie führt das Testing über ARIA-Attribute (Accessible Rich Internet Applications) als bevorzugte Art durch.

Die Library ist für alle gängigen JavaScript- und End-to-End-Testing-Frameworks wie TestCafe, Cypress und Puppeteer verfügbar.

Folgender Code implementiert die drei Ansätze:

import { Selector } from 'testcafe';

// Version 1: using testid
const valueEl = Selector("[data-testid='count:value']");
const incrementBtn = 
  Selector("[data-testid='count:increment']");
const decrementBtn = 
  Selector("[data-testid='count:decrement']");

// Version 2: using accessibility labels    
const valueEl = Selector("[aria-label='Value']");
const incrementBtn = 
  Selector("[aria-label='Increment value']");
const decrementBtn = 
  Selector("[aria-label='Decrement value']");

import { screen } from '@testing-library/testcafe'

// Version 3: using testing library
const valueEl = screen.getByRole('presentation');
const incrementBtn = 
  screen.getByLabelText('Increment value');
const decrementBtn = 
  screen.getByLabelText('Decrement value');

Ein Skript führt einen im Ordner test/e2e abgelegten Test mit TestCafe aus:

testcafe chrome test/e2e/

TestCafe ruft wie angefordert Chrome auf und klickt die Anwendung durch (s. Abb. 2). Neben Chrome unterstützt TestCafe alle modernen Browser auf Desktop und Mobilgeräten sowie zusätzlich IE11. Chrome und Firefox erlauben optional die Headless-Nutzung und Chrome zusätzlich die Device-Emulation für mobile Geräte. "Echte" Mobilgeräte müssen für den Test, der auch im durchlaufenden Watch-Modus möglich ist, über eine URL manuell mit dem Test-Server verbunden sein. Die von TestCafe unterstützten Cloud-Testing-Dienste Sauce Labs und BrowserStack ermöglichen das Ausführen der Anwendung auf Tausenden realer Geräte.

Die breite Unterstützung von Browsern hebt TestCafe von der Konkurrenz ab. Puppeteer spielt nur mit Chrome zusammen und Cypress mit Chrome, Edge und Firefox. TestCafe kombiniert einen Node.js-Server-Part mit einem Teil, der direkt im Browser läuft. In Kombination mit Headless-Browsern oder den erwähnten Cloud-Testing-Diensten laufen die Tests umstandslos im Build auf einem CI-Server.

TestCafe führt einen Test in Chrome aus (Abb. 2).

Da TestCafe innerhalb des Browsers läuft, kann es automatisch auf die Antwort von einem XMLHttpRequest (XHR) oder einer fetch-Anfrage warten. Das vereinfacht Tests, die Remote-Calls auslösen: Sie enthalten keine zusätzlichen Wait-Anweisungen und sind im Code nicht von synchronen Tests zu unterscheiden. TestCafe wartet, bis der Request beendet ist oder drei Sekunden abgelaufen sind. In letzterem Fall löst er einen Timeout und Fehlschlag des Tests aus. Erwartungsgemäß längere Request-Zeiten lassen sich im Test angeben. Im Fehlerfall kann TestCafe auch bei einem CI-Build automatisch einen Screenshot anfertigen oder im interaktiven Modus den Test anhalten und ein Debugging im Browser zulassen.