Inhaltsverzeichnis


Nichts verpassen?

cypress testing typescript | David Würfel Twitter Logo | 13 Minuten

Der folgende Artikel möchte zeigen, wie ihr End-to-End Testing für Angular mit Cypress und TypeScript aufsetzt. Wir werden erste E2E Tests schreiben und sie zur Ausführung auf CircleCI als Continunous Integration System vorbereiten, sodass sie mit jedem Push auf euer Repository ausgeführt werden.

Bevor wir starten: Was ist ein E2E Test?

End-to-End (kurz E2E) Testing ist eine Art des Software Testings, welches nicht nur das Software-System an sich validiert, sondern auch dessen Zusammenspiel mit externen Schnittstellen und deren Intergation. E2E Tests schreibt man, um vollständige Szenarien durchzuspielen, die einem Produktivsystem entsprechen.

Quelle (frei übersetzt): https://www.guru99.com/end-to-end-testing.html

Überblick

Früher habe ich in der Frontend Entwicklung mit .NET und WPF von Microsoft gearbeitet und erinnere mich noch gut an die Zeiten, in denen wir kostenintensive Frameworks zum Schreiben von End-to-End Tests für unsere Projekte evaluiert haben. Nach vielen Wochen, sogar Monaten der Evaluation, der Entwicklung von speziellem Glue-Code oder einer eigenen Test-Infrastruktur auf Basis existierender Werkzeuge, hatten wir es endlich geschafft, unsere E2E Tests zum Laufen zu bekommen. Leider waren sie ziemlich brüchig und sind oft fehlgeschlagen. Wir mussten händische Anpassungen machen oder hatten Probleme mit unzuverlässigen Test-Runnern in unserer Continuous Integration Pipeline.

Ein paar Jahre später mit Angular und Protractor als voreingestelltem E2E Testing-Framework sind die Tests weiterhin basierend auf sogenannten Page Objects und dem Selenium Web Driver. Unzuverlässig sind sie immer noch. Keine teuren, kommerziellen Frameworks oder maßgeschneiderte Infrastruktur war mehr notwendig. Aber hat es Spaß gemacht E2E Tests zu schreiben? Nein.

Mittlerweile sind wir aber im Jahr 2020 angekommen und die Zeit zum Aufstieg neuer Helden ist angebrochen. 🚀

Was ist Cypress

Cypress verspricht schnelles, einfaches und zuverlässiges Testing für Alles, was im Browser läuft. Es basiert nicht auf dem Selenium Web Driver, der Netzwerk Verbindungen nutzt, um mit dem Browser zu interagieren. Stattdessen ist Cypress ein Test Runner, der im Inneren eures Browsers neben der Web Applikation läuft und darum direkten Zugriff auf diese hat.

Ohne an dieser Stelle auf alle Details einzugehen, ist dadurch Cypress nicht nur schneller und zuverlässiger, es ebnet auch den Weg für viele weitere interessante Eigenschaften wie beispielsweise

  • Debugging mit Zeitreisefunktion
  • Einfaches Aufzeichnen von Videos und Screenshots des Testlaufs
  • Automatisierte Wartemechanismen

Zusätzlich zu all diesen Besonderheiten hat Cypress eine Developer Experience (DX), die kaum zu toppen ist. Habt ihr jemals eine Fehlermeldung gesehen, die euch genau sagt, was ihr falsch gemacht habt, euch aufzeigt, welche Abhängigkeiten hinzugefügt werden müssen und euch darüber hinaus noch zu einer verständliche Dokumentation leitet, die das Problem im Detail beschreibt? So fühlt sich Cypress an: Es ist gemacht von Entwicklern für Entwickler.

Cypress Fehlermeldung auf dem Build-Server, die genau sagt, wie der Fehler zu beheben sei

Gleich werden wir Cypress für ein frisches, mit der CLI erstelltes Angular Projekt installieren. Wir werden ein paar E2E Test schreiben und sie am Ende von einem automatisierten Build-System laufen lassen. Die Gesamtheit dieser Schritte sollte nicht länger als 60 Minuten in Anspruch nehmen. Wir werden versuchen, die Schritte so kurz wie möglich zu halten und existierende Werkzeuge wie Angular Schematics, Bibliotheken oder bekannte Vorlagen zu unserem Vorteil einzusetzen.

Vorbedingungen

Diese Anleitung setzt voraus, dass wir bereits mit einem standardmäßigen Angular 9 Projekt arbeiten. Falls dies noch nicht der Fall ist, können wir ein neues mit der Angular CLI erstellen. Wenn wir die CLI nicht global installiert haben, können wir uns den npx Befehl zu Nutze machen, der die CLI temporär installiert:

npx @angular/cli new <app-name>

Cypress aufsetzen

Um Cypress zusammen mit TypeScript so schnell wie möglich aufzusetzen, nutzen wir eine von BrieBug entwickelte Schematic.

Im Wurzelverzeichnis des Angular Projekts öffnen wir das Terminal und geben folgenden Befehl ein:

ng add @briebug/cypress-schematic --addCypressTestScripts

Falls die CLI nicht global installiert ist, könnte es sein, dass der ng Befehl nicht direkt verfügbar ist. Wir können den Aufruf des lokalen ng aus der package.json erzwingen:

# Falls 'ng' allein nicht gefunden werden konnte
npm run ng -- add @briebug/cypress-schematic

Wir können ohne Bedenken Protractor entfernen, weil es vollständig ersetzt wird. Während der Installation werden ein paar Binardateien heruntergeladen, da der Cypress Test-Runner eine in Electron gepackte Oberfläche ist.

Durch die Verwendung des Parameters --addCypressTestScripts werden zwei nützliche npm Skripte hinzugefügt, die das Arbeiten mit Cypress komfortabler machen: Eins, um die E2E Tests mit, das andere, um die Tests ohne die Benutzeroberfläche auszuführen.

    // package.json Skripte

    "cy:run": "cypress run",
    "cy:open": "cypress open"

Würde man eines dieser Skripte alleinstehend laufen lassen, würde der Test zunächst fehlschlagen, weil er versuchen würde auf http://localhost:4200 zu navigieren, wo aktuell noch nichts bereitgestellt wird. Um das zu beheben, können wir ein zweites Konsolenfenster öffnen und unsere Angular Applikation vorher mit npm start bereitstellen.

Glücklicherweise passt die vorherige Schematic den e2e Befehl so an, dass dies für uns automatisch von einem sogenannten CLI Builder durchgeführt wird. Mit folgendem Befehl liefern wir die Anwendung aus und lassen den Test dagegen laufen:

npm run e2e

Cypress erkennt, dass es zum ersten Mal ausgeführt wird. Es verifiziert seine Installation und fügt ein paar grundlegende Beispieldateien hinzu. Nachdem die Oberfläche startet, können wir bereits einen Test sehen, der für uns erstellt wurde.

Cypress Oberfläche nach cy:open oder e2e

Der Test wird ausgeführt, sobald wir ihn auswählen. Initial wird er fehlschlagen, weil wir tatsächlich noch nichts wirklich testen. Das werden wir nun beheben.

Fehlschlagender Test, der aktuell noch nicht implementiert ist

Erste Tests

Wie von den Cypress Best Practices vorgeschlagen, setzen wir als ersten Schritt die globale Basis-URL, sodass wir diese Adresse nicht bei jeder Testausführung duplizieren müssen. Dazu fügen wir die folgende Konfiguration der Datei cypress.json hinzu:

// cypress.json
{
  "baseUrl": "http://localhost:4200"
}

Danach schreiben wir unseren ersten Smoke Test, der nur prüft, ob der standardmäßige Titel der Applikation auf der Angular Startseite gesetzt ist. Dazu ändern wir den Inhalt von spec.ts auf folgenden:

// spec.ts
it('smoke test', () => {
  cy.visit('/');
  cy.contains('cypress-e2e-testing-angular app is running!');
});

Der Test startet, indem er zu unserer Basis-URL navigiert und nach einem beliebigem Element sucht, dass den Text cypress-e2e-testing-angular app is running! enthält.

Nutzerverhalten testen

Obwohl dieser Test bereits funktioniert, möchten wir nun etwas interaktivere schreiben. Weil E2E Tests von Natur aus langsamer sind als Unit Tests, ist es völlig in Ordnung solche zu schreiben, die einen gesamten Ablauf eines Benutzerverhaltens bezogen auf eine Funktionalität modellieren.

Nun wollten wir bespielsweise prüfen, ob die Eigenschaften der eben erwähnten Startseite gültig sind: Unsere Seite sollte ihren Titel anzeigen und darunter im Konselenfenster den Text ng generate. Klickt der Nutzer dann auf den Angular Material Button, wollen wir sicherstellen, dass das richtige Kommando ng add in der Konsolenansicht darunter erscheint.

Wir ersetzen den Inhalt unserer Testdatei mit folgendem Inhalt:

// spec.ts
describe('When Angular starting page is loaded', () => {
  beforeEach(() => {
    cy.visit('/');
  });

  it('has app title, shows proper command by default and reacts on command changes', () => {
    cy.contains('cypress-e2e-testing-angular');

    cy.contains('.terminal', 'ng generate component xyz');

    cy.contains('Angular Material').click();
    cy.contains('.terminal', 'ng add @angular/material');
  });
});

Unsere Testreihe haben wir etwas umgestellt, indem wir einen describe Block hinzugefügt haben, um alle Tests zu gruppieren, die sich auf unsere Startseite beziehen. Da wir in jedem Test die Basis-URL besuchen, haben wir dies in den beforeEach Aufruf verschoben. Zu guter Letzt kombinieren wir unseren existierenden Smoke Test mit dem Test für die Konsolenfenster-Ansicht der Startseite.

Es ist wichtig zu wissen, dass man die Ergebnisse der Cypress Anfragen nicht in Variablen zwischenspeichert, sondern stattdessen Closures verwenden sollte. Darüber hinaus wählen wir aktuell unsere Element über CSS Klassen oder sogar Textinhalt aus, was sehr brüchig sein kann. Hier wird empfohlen data- Attribute zur Auswahl von Elementen zu nutzen.

Cypress hat eine Menge toller Funktionalitäten und Möglichkeiten. Wir können hier nicht alle von ihnen behandeln, da unser Fokus darauf liegt, einen ersten Startpunkt zu geben. Die sehr gute, offizielle Dokumentation liefert alle Informationen, wie man mit verschiedenen Elementen interagiert.

Lassen wir unsere Testreihe nochmal laufen, sollten wir sehen, wie sich Cypress durch jedes Szenario durchklickt. Unsere 3 Prüfungen sollten diesmal gelingen. ✔✔✔

Unsere erste Cypress Testreihe erfolgreich laufen lassen

Continuous Integration aufsetzen

Aktuell laufen unsere Tests nur lokal, daher möchten wir als Nächstes eine kleine CI (Continuous Integration) Pipeline aufsetzen. Ein guter Weg sich darauf vorzubereiten, ist es, ein paar npm Skripte anzulegen und diese zu kombinieren, sodass das Build-System ein einziges Skript als Einstiegspunkt aufrufen kann. Wenn wir diese Methode verfolgen, können wir auch die CI Schritte vor dem Push auf den Server zuvor lokal ausprobieren. Weiterhin sind diese Skripte recht unabhängig von einem tatsächlich verwendeten Build-System.

Auf dem CI-Server müssen wir den Webserver im Hintergrund starten und darauf warten, dass unsere Anwendung gebaut wird, was etwas dauern kann. Danach muss der Cypress Test-Runner gestartet werden, durch alle Tests gehen und nach Abschluss der Tests den Server herunterfahren. Praktischerweise gibt es dafür das Hilfsmittel namens start-server-and-test, wie in der Cypress Dokumentation beschrieben.

npm install --save-dev start-server-and-test

Nachdem es installiert ist, können wir den Angular serve Befehl nutzen, der über npm start aufgerufen wird und ihn mit dem cy:run Kommando verbinden, der die Tests ohne Start einer Benutzeroberfläche ausführt.

   // package.json Skripte
  "start": "ng serve",
  "cy:run": "cypress run",
  "e2e:ci": "start-server-and-test start http://localhost:4200 cy:run"

Selbstverständlich könnte man auch einen Produktiv-Build nutzen oder vorher einen Build ausführen und die Applikation über einen beliebigen Http-Server ausliefern. Allerdings möchten wir es hier kurz halten und überlassen diese Verbesserung dem Leser.

Circle CI

Für unser Beispiel nutzen wir CircleCI, weil es sich gut mit GitHub integriert, weit verbreitet ist und eine freie Nutzung erlaubt. Die letztliche Wahl eines CI-Systems wie Jenkins oder GitLab(mit dem ich die meiste Erfahrung gesammelt habe) ist euch selbst überlassen. Nachdem wir uns bei CircleCI angemeldet und unseren GitHub Account verbunden haben, können wir ein Repository auswählen und ein neues Projekt über das CircleCI-Dashboard erstellen.

Um die Pipeline zu konfigurieren, könnten wir durch Auswahl einer Vorlage die config.yml selbst schreiben und sie auf unsere Bedürfnisse hin anpassen, um schlussendlich unser E2E Skript aufzurufen. Erfreulicherweise bringt Cypress bereits eine fertig nutzbare Konfiguration (als sogenannten Orb) für CircleCI mit, welche bereits die Installation von Abhängigkeiten, Caching und mehr beinhaltet. Bevor wir diese allerdings nutzen können, müssen wir erst für CircleCI über die Organisation Settings die Drittanbieter Runner aktivieren:

# circleci/config.yml

version: 2.1
orbs:
  # This Orb includes the following:
  # - checkout current repository
  # - npm install with caching
  # - start the server
  # - wait for the server to respond
  # - run Cypress tests
  # - store videos and screenshots as artifacts on CircleCI
  cypress: cypress-io/cypress@1
workflows:
  build:
    jobs:
      - cypress/run:
          start: npm start
          wait-on: 'http://localhost:4200'
          store_artifacts: true

Die Pipeline hat nur eine Aufgabe: Führe alle E2E Tests aus. Der aktuelle Git-Branch wird ausgecheckt, alle Abhängigkeiten inklusive Caching installiert, der Applikations-Server wird gestartet und die Tests werden ausgeführt. Zusätzlich werden standardmäßig Videos des Testdurchlaufs und Screenshots (im Falle von fehlschlagenden Tests) aufgezeichnet und am Ende als CircleCI Artefakte zur späteren Analyse hochgeladen.*

CircleCI Dashboard

Fazit

Die Schritte in dieser Anleitung sind sehr minimal gehalten. Ihr könntet euer existierendes Angular Projekt nutzen, ihr könnt die Konfiguration der Cypress Testreihen ändern oder deutlich mehr und aussagekräftigere Tests schreiben. Darüber hinaus könnte es euch nützen, npm Skripte für unterschiedliche Szenarien oder Umgebungen zu schreiben und natürlich die gesamte Build-Pipeline mit automatischen Codeprüfungen, Unit Tests, dem Bauprozess an sich oder der Auslieferung eurer Anwendung zu erweitern. Dennoch sollte dies ein erster Schritt sein, der zeigt, wie schnell ihr automatisierte End-to-End Tests heutzutage aufsetzen könnt.

Wartet, bis ihr eure ersten echten Cypress Tests für eure Anwendung schreibt. Ihr werdet Spaß haben!

Animiertes Bild, das eine überwältigte Person zeigt

Ich hoffe, ihr konntet etwas nützliches aus dem Artikel ziehen. Bei Fragen oder Anmerkungen, lasst es mich wissen. Rückmeldung ist immer sehr willkommen!

Der Quellcode für diese Anleitung ist auf GitHub zu finden: https://github.com/MrCube42/cypress-e2e-testing-angular

Quellen und Hinweise

Für andere CI-System könnte man unser vorher definiertes npm Skript nutzen. Jedoch müssten wir uns dann um all die zusätzlichen Hilfsmittel selbst kümmern. Falls ihr bereits eine weit entwickelte Pipeline habt, könnte es ein einfacherer Weg sein, nur dieses eine Skript zu integrieren.