Was beim Schreiben von E2E-Tests zu beachten ist #frontend@twiliosendgrid

Veröffentlicht: 2020-09-19

Bei Twilio SendGrid schreiben wir End-to-End (E2E)-Tests gegen Ende eines neuen Funktions- oder Seitenentwicklungszyklus, um sicherzustellen, dass alle Teile miteinander verbunden sind und zwischen Frontend und Backend aus Sicht des Endbenutzers ordnungsgemäß funktionieren.

Wir haben über zwei Jahre lang mit verschiedenen E2E-Testframeworks und -bibliotheken experimentiert, wie z Die Lösungen. Unabhängig davon, welches Framework oder welche Bibliothek wir verwendet haben, stellten wir uns die gleichen Fragen darüber, welche Funktionen wir automatisieren und für die wir E2E-Tests schreiben können. Nachdem wir festgestellt hatten, welche Funktionen wir testen können, bemerkten wir auch, dass wir beim Schreiben und Einrichten der Tests immer wieder dieselbe allgemeine Strategie anwendeten.

Dieser Blogbeitrag erfordert keine Vorkenntnisse zum Schreiben von E2E-Tests in einer bestimmten Bibliothek oder einem bestimmten Framework, aber er hilft, wenn Sie Webanwendungen gesehen und sich gefragt haben, wie Sie Dinge im Browser am besten automatisieren können, um zu testen, ob die Seiten korrekt funktionieren. Wir möchten Sie durch die Denkweise von E2E-Tests führen, damit Sie diese Fragen und die allgemeine Strategie zum Schreiben von Tests auf jedes Framework Ihrer Wahl anwenden können.

Fragen, die Sie sich stellen sollten, wenn Sie einen E2E-Test automatisieren können

Beim Schreiben von E2E-Tests müssen wir sicherstellen, dass die Flows auf den Seiten, die wir in unserer Anwendung testen, bestimmte Kriterien erfüllen. Lassen Sie uns einige der allgemeinen Fragen durchgehen, die wir uns stellen, um festzustellen, ob ein E2E-Test automatisiert werden kann.

1. Ist es möglich, die Benutzerdaten vor jedem Test durch eine zuverlässige Methode wie die API auf einen bestimmten Zustand zurückzusetzen? Wenn es keine Möglichkeit gibt, einen Benutzer zuverlässig auf den gewünschten Status zurückzusetzen, kann er nicht automatisiert werden und es wird erwartet, dass er als Teil Ihrer Blockierungstests vor der Bereitstellung ausgeführt wird. Es ist auch ein Antimuster und normalerweise nicht deterministisch, einen Benutzer über die Benutzeroberfläche in einen bestimmten Zustand zurückzuversetzen, da dies langsam ist und die Automatisierung von Schritten durch die Benutzeroberfläche bereits unzuverlässig genug ist. Es ist zuverlässiger, API-Aufrufe durchzuführen, um den Status des Benutzers zurückzusetzen, ohne jemals eine Seite im Browser öffnen zu müssen. Eine andere Alternative, wenn Sie einen bestehenden Dienst haben, besteht darin, vor jedem Test neue Benutzer mit den richtigen Daten zu erstellen. Solange wir vor jedem Test einen dauerhaften Benutzer zurücksetzen oder einen Benutzer erstellen, können wir uns dann auf die Teile konzentrieren, die wir auf der Seite testen.

2. Haben wir die Kontrolle über die Funktion, API oder das System, das wir testen möchten? Wenn es sich um einen Dienst eines Drittanbieters handelt, auf den Sie sich für die Abrechnung oder für andere Funktionen verlassen, gibt es eine Möglichkeit, ihn zu verspotten oder ihn deterministisch mit bestimmten Werten arbeiten zu lassen? Sie möchten so viel Kontrolle wie möglich über den Test erlangen, um die Flockigkeit zu reduzieren. Sie können dedizierte Testbenutzer mit isolierten Ressourcen oder Daten pro Testlauf erstellen, sodass sie von nichts anderem beeinflusst werden.

3. Ist der Dienst oder die Funktion selbst konsistent genug, um innerhalb eines angemessenen Zeitlimits zu funktionieren? Häufig müssen Sie möglicherweise Abfragen implementieren oder warten, bis bestimmte Daten verarbeitet und in die Datenbank aufgenommen werden (z. B. langsamere asynchrone Aktualisierungen und ausgelöste E-Mail-Ereignisse). Wenn diese Dienste häufig innerhalb eines angemessenen und zuverlässigen Zeitfensters ausgeführt werden, können Sie geeignete Abfrage-Timeouts festlegen, während Sie darauf warten, dass bestimmte DOM-Elemente angezeigt oder Daten aktualisiert werden.

4. Können wir die Elemente auswählen, mit denen wir auf einer Seite interagieren müssen? Haben Sie es mit Iframes oder generierten Elementen zu tun, über die Sie keine Kontrolle haben und die Sie nicht ändern können? Um mit Elementen auf einer Seite zu interagieren, können Sie spezifischere Selektoren wie „data-hook“- oder „data-testid“-Attribute hinzufügen, anstatt nach IDs oder Klassennamen zu selektieren. Die IDs und Klassennamen sind anfälliger für Änderungen, da sie häufig Stilen zugeordnet sind. Stellen Sie sich vor, Sie versuchen, auf andere Weise gehashte Klassennamen oder IDs aus formatierten Komponenten oder CSS-Modulen auszuwählen. Für von Drittanbietern generierte Elemente oder Open-Source-Komponentenbibliotheken wie „react-select“ könnten Sie diese Elemente mit einem übergeordneten Element mit einem „data-hook“-Attribut umschließen und die untergeordneten Elemente darunter auswählen. Für den Umgang mit iFrames haben wir benutzerdefinierte Befehle erstellt, um die DOM-Elemente zu extrahieren, die wir bestätigen und auf die wir reagieren müssen, wofür wir später ein Beispiel geben werden.

Es sind noch weitere Überlegungen zu berücksichtigen, aber es läuft alles auf eine Frage hinaus: Können wir diesen E2E-Test konsistent und zeitnah wiederholen und die gleichen Ergebnisse erzielen?

Allgemeine Strategie zum Schreiben von E2E-Tests

1. Finden Sie die hochwertigen Testfälle heraus, die wir automatisieren können . Einige Beispiele umfassen Happy-Path-Tests, die den größten Teil eines Funktionsablaufs abdecken: Durchführen von CRUD-Vorgängen über die Benutzeroberfläche für die Profilinformationen eines Benutzers, Filtern einer Tabelle nach übereinstimmenden Ergebnissen bei bestimmten Daten, Erstellen eines Beitrags oder Einrichten eines API-Schlüssels. Andere Grenzfälle und die Fehlerbehandlung können jedoch besser mit Einheiten- und Integrationstests abgedeckt werden. Gehen Sie die Fragen durch, die wir im vorherigen Abschnitt erwähnt haben, um Ihre Liste der Testfälle zu verkürzen.

2. Denken Sie darüber nach, wie Sie diese Tests wiederholen können, indem Sie so viel wie möglich über die API einrichten oder herunterfahren. Beginnen Sie bei den hochwertigen, automatisierbaren Testfällen damit, zu notieren, welche Dinge Sie über die API einrichten sollten. Einige Beispiele sind das Seeding des Benutzers mit richtigen Daten, wenn der Benutzer nicht über genügend filterbare Daten für die Paginierung verfügt, wenn die Daten des Benutzers in einem fortlaufenden Fenster von 30 Tagen ablaufen oder wenn wir möglicherweise einige Daten löschen müssen, die nach erfolgreicher oder unvollständiger Ausführung übrig geblieben sind Tests, bevor der aktuelle Test erneut beginnt. Die Tests sollten unabhängig davon, ob der letzte Testlauf erfolgreich war oder fehlgeschlagen ist, in demselben wiederholbaren Zustand ausgeführt und eingerichtet werden können.

Es ist wichtig zu überlegen: Wie kann ich die Daten dieses Benutzers auf den Ausgangspunkt zurücksetzen, damit ich nur den Teil der Funktion testen kann, den ich möchte?

Wenn Sie beispielsweise testen möchten, ob ein Benutzer einen Beitrag hinzufügen kann, damit er schließlich in der Beitragsliste des Benutzers angezeigt wird, muss der Beitrag zuerst gelöscht werden.

3. Versetzen Sie sich in die Lage Ihres Kunden und verfolgen Sie die UI-Schritte, die erforderlich sind, um einen Feature-Flow vollständig abzuschließen. Zeichnen Sie die Schritte für einen Kunden auf, um einen vollständigen Ablauf oder eine vollständige Aktion abzuschließen. Verfolgen Sie nach jedem Schritt, was der Benutzer sehen oder nicht sehen oder womit er interagieren sollte. Wir werden währenddessen Plausibilitätsprüfungen und Behauptungen durchführen, um sicherzustellen, dass die Benutzer die richtigen Abfolgen von Ereignissen für ihre Aktionen erleben. Wir werden dann die Plausibilitätsprüfungen in automatisierte Befehle und Behauptungen übersetzen.

4. Pflegen Sie Änderungen und automatisieren Sie Abläufe, indem Sie bestimmte Selektoren hinzufügen und Seitenobjekte (oder jede andere Art von Wrapper) implementieren. Überprüfen Sie die Schritte, die Sie notiert haben, um zu manövrieren und einen Feature-Flow zu durchlaufen. Fügen Sie spezifischere Selektoren wie "Datenhaken"-Attribute zu Elementen hinzu, mit denen der Benutzer wie Schaltflächen, Modale, Eingaben, Tabellenzeilen, Warnungen und Karten interagiert hat. Wenn Sie möchten, können Sie Seitenobjekte, Wrapper oder Hilfsdateien mit Verweisen auf diese Elemente über die von Ihnen hinzugefügten Selektoren erstellen. Anschließend können Sie wiederverwendbare Funktionen implementieren, um mit den umsetzbaren Elementen der Seite zu interagieren.

5. Übersetzen Sie die aufgezeichneten Benutzerschritte mit den von Ihnen erstellten Helfern in Cypress-Tests. In den Tests melden wir uns normalerweise über die API bei einem Benutzer an und speichern das Sitzungscookie, bevor jeder Testfall ausgeführt wird, um angemeldet zu bleiben. Anschließend richten wir die Daten des Benutzers über die API ein oder löschen sie, um einen konsistenten Ausgangspunkt zu haben. Wenn alles vorhanden ist, besuchen wir die Seite, die wir direkt auf unsere Funktion testen werden. Wir fahren mit der Ausführung von Schritten für den Flow fort, wie z. B. einem Flow zum Erstellen, Aktualisieren oder Löschen, und geben an, was dabei passieren oder auf der Seite sichtbar sein soll. Um Tests zu beschleunigen und Unregelmäßigkeiten zu reduzieren, vermeiden Sie das Zurücksetzen oder Aufbauen des Status über die Benutzeroberfläche und umgehen Dinge wie das Anmelden über die Anmeldeseite oder das Löschen von Dingen über die Benutzeroberfläche, um sich auf die Teile zu konzentrieren, die Sie testen möchten. Stellen Sie sicher, dass Sie diese Teile immer in den Hooks „before“ oder „beforeEach“ ausführen. Andernfalls können die Tests zwischendurch fehlschlagen, wenn Sie „after“- oder „afterEach“-Hooks verwendet haben, was dazu führt, dass Ihre Bereinigungsschritte nie ausgeführt werden und nachfolgende Testläufe fehlschlagen.

6. Testflockigkeit hämmern und ausstanzen. Nachdem Sie die Tests implementiert und einige Male lokal bestanden haben, ist es verlockend, eine Pull-Anforderung einzurichten, sie sofort zusammenzuführen und die Tests nach einem Zeitplan mit dem Rest Ihrer Testsuite auszuführen oder sie in Ihren Bereitstellungsschritten auszulösen. Bevor Sie das tun:

    1. Versuchen Sie zunächst, den Benutzer in verschiedenen Zuständen zu belassen, und prüfen Sie, ob Ihre Tests noch bestanden werden, um sicherzustellen, dass Sie die richtigen Einrichtungsschritte ausführen.
    2. Untersuchen Sie als Nächstes, ob Sie Ihre Tests parallel ausführen, wenn sie während eines Ihrer Bereitstellungsabläufe ausgelöst werden. Auf diese Weise können Sie sehen, ob dieselben Benutzer auf Ressourcen herumtrampeln und ob Race-Conditions vorliegen.
    3. Beobachten Sie dann, wie Ihre Tests im Headless-Modus in einem Docker-Container ausgeführt werden, um festzustellen, ob Sie möglicherweise Timeouts erhöhen oder Selektoren anpassen müssen.

Das Ziel ist es, zu sehen, wie sich Ihre Tests bei wiederholten Testläufen unter verschiedenen Bedingungen verhalten, und sie so stabil und konsistent wie möglich zu gestalten, damit wir weniger Zeit damit verbringen, die Tests zu beheben, und uns mehr darauf konzentrieren, tatsächliche Fehler in unseren Umgebungen zu finden.

Hier ist ein Beispiellayout für Cypress-Testbausteine, in dem wir einen globalen Anmeldebefehl namens „cy.login(username, password)“ erstellt haben. Wir setzen das Cookie ausdrücklich und bewahren es vor jedem Testfall auf, damit wir angemeldet bleiben und direkt loslegen können zu den Seiten, die wir testen. Wir führen auch einige Einrichtungs- oder Abbauvorgänge über die API durch und umgehen jedes Mal die Anmeldeseite, wie unten gezeigt.

Gedanken beenden

Neben dem Vergleich, welche E2E-Lösung am besten geeignet ist, ist es auch wichtig, die richtige Denkweise für E2E-Tests anzunehmen. Es ist entscheidend, zuerst Fragen zu stellen, ob das Feature, das Sie testen möchten, den Anforderungen entspricht, die automatisiert werden sollen. Es sollte Möglichkeiten geben, den Benutzer oder die Daten zuverlässig auf einen bestimmten Zustand zurückzusetzen (z. B. über die API), damit Sie sich auf das konzentrieren können, was Sie zu validieren versuchen.

Wenn es keine zuverlässige Möglichkeit gibt, Ihren Benutzer oder Ihre Daten auf den richtigen Ausgangspunkt zurückzusetzen, sollten Sie sich mit Erstellungstools und APIs zum Erstellen von Benutzern mit bestimmten Konfigurationen befassen. Sie können auch erwägen, die Dinge zu verspotten, die Sie kontrollieren können, um die Tests so stabil und konsistent wie möglich zu machen. Andernfalls sollten Sie den Wert und die Kompromisse mit Ihrem Team abwägen. Ist diese Funktion eine Funktion, die Sie Unit-Tests oder manuellen Regressionstests überlassen sollten, wenn neue Codeänderungen gepusht werden?

Für die Funktionen, die Sie in E2E-Tests automatisieren können, ist es oft am wertvollsten, die wichtigsten Happy-Path-Flows Ihrer Benutzer abzudecken. Machen Sie die Dinge noch einmal zeitnah und konsistent wiederholbar und beseitigen Sie Unregelmäßigkeiten, wenn Sie E2E-Tests mit einem beliebigen Framework oder einer beliebigen Bibliothek schreiben.

Weitere Informationen zu speziellen E2E-Tests von Cypress finden Sie in den folgenden Ressourcen:

  • 1.000-Fuß-Übersicht über das Schreiben von Cypress-Tests
  • Schreiben Sie alle Dinge in Ihre Cypress-Tests
  • Umgang mit E-Mail-Flows in Cypress-Tests
  • Ideen zum Konfigurieren, Organisieren und Konsolidieren Ihrer Cypress-Tests
  • Integration von Cypress-Tests mit Docker, Buildkite und CICD