Normalerweise ist mein Thema ja eher Angular und das komplette Ökosystem dazu. Aber auch Testing ist mir eine Herzensangelegenheit. Ich möchte in diesem Artikel eine Möglichkeit präsentieren, wie man Webanwendungen testen kann. Wir schauen uns in diesem Zusammenhang das Tool Cypress an. Wir lernen, wie man Cypress in den Angular-Workspace integriert und es nutzt. Mein Ziel ist es, möglichst viele Entwickler für Tests zu begeistern.

Weitere Experten-Beiträge zu Angular finden Sie in unserem kostenlosen E-Book

Beim Thema Testen muss man zwischen zwei Arten unterscheiden.

  • Unit Tests (Code based)
  • E2E Tests (User flow)

Was sind Unit Tests?

Unit Tests sind dem einen oder anderen eventuell auch als Module Tests bekannt. Hierbei werden einzelne Teile einer Anwendung (Units) getestet. So ist es zum Beispiel möglich, einen Service zu testen, ohne dass die komplette Anwendung (oder gar das Backend) laufen muss. Dadurch ist etwas Wunderbares möglich: Test Driven Development (TDD).

Ich schreibe also einen Test für meinen Service, wie er am Ende funktionieren muss. Dann lasse ich den Test laufen und implementiere solange, bis der Test grün ist. Dadurch reduziere ich die Gefahr, zu viel Logik zu implementieren. Hierfür kann man verschiedene Tools nutzen, z.B.: Mocha, Jasmine und Jest. Gerade in Agilen Teams sind Unit Tests verbreitet.

Vorteile:

  • Fehler in der Implementierung werden schnell entdeckt.
  • Ein Test kann auch als Dokumentation dienen.

Nachteile:

  • Kann sehr zeitraubend sein, gerade wenn das erwartete Verhalten erst (wasserfallartig) zur Entwicklungszeit definiert wird.
  • Bei Refactoring müssen meist alle Tests neu geschrieben werden.

Was sind E2E Tests?

E2E ist die Kurzform für End-to-End Tests. Sie werden im Gegensatz zu Unit Tests auf die komplette Applikation angewendet. Wie der Begriff End-To-End vermuten lässt, können alle Layer in den Test einbezogen werden. Vom User Interface (UI) bis zum Backend. Ich kann zum Beispiel testen, ob Interaktionen in der UI die erwarteten Aktionen im Backend ausführen (CRUD von Daten).

Wir reden hier von Tests, die ganze User-Stories abdecken. Diese Tests werden meist am Ende der Entwicklung (eines Features) geschrieben. Wenn ich ein Refactoring der Applikation vorhabe, sollte ich eine möglichst hohe Testabdeckung haben, damit ich sicherstellen kann, dass das Refactoring nicht den Flow der Applikation zerstört hat. Hierfür kann man verschiedene Tools nutzen, z.B.: Cypress, Protractor, Nightwatch.js, und Puppeteer.

Vorteile:

  • Ich kann sicherstellen, dass meine Applikation wie erwartet funktioniert (durch alle Layer hindurch).
  • Es werden ‘echte’ User-Interaktionen getestet.

Nachteile:

  • Diese Tests sind sehr fragil, wenn das UI sich ändert.
  • Es besteht die Gefahr, dass diese Tests (da sie am Ende eines Entwicklungszyklus geschrieben werden) beim Projektverantwortlichen den Anschein erwecken, man könne sie doch auch leicht durch menschliche Tester ersetzen (irgendwann testet der das dann eventuell auch).

Sie sehen sehen, Unit Tests und E2E Tests haben ihre eigenen Daseinsberechtigungen und sind am besten in Kombination anzuwenden.

In diesem Artikel möchte ich über E2E Testing schreiben. Ich bin vor einiger Zeit auf Cypress aufmerksam geworden und finde es super.

Was ist Cypress?

Cypress ist ein E2E Testing Tool, das mit einer eigenen Electron App kommt, in der die zu testende App ausgeführt wird.

Abbildung - Angular-Testing - Das E2E Testing Tool Cypress

Der Vorteil daran: Cypress verhält sich wie ein Interceptor oder Proxy. Alle Kommunikation, die von der zu testenden App ausgeht, kann von Cypress überwacht, manipuliert und gemockt werden.

Was alle E2E Tools gemeinsam haben, ist, dass sie nicht für ein bestimmtes JavaScript Framework gebaut wurden. Man kann damit jede Web App testen.

Wie kann ich Cypress für meine Angular App nutzen?

Ich starte hier mit einem frisch begonnenen Angular Projekt (ng new cypress-test).

Cypress ist Framework-unabhängig, d.h. es können alle Webseite damit getestet werden, es muss keine Angular App sein.

Mit Angular hat man den großen Vorteil, dass man dank der CLI die Umstellung von Protractor (als Standard E2E Testing Tool) zu Cypress vollautomatisch durchführen kann.

Voraussetzung ist ein Projekt, welches mit der CLI Version 6+ gebaut wurde.

Alles, was wir dafür tun müssen, ist der CLI zu sagen, sie solle bitte Cypress installieren:

ng add @briebug/cypress-schematic

Dies veranlasst die CLI dazu, Cypress zu installieren (npm install cypress) und das Projekt für Cypress zu konfigurieren.

Die CLI fragt dann, ob Protractor entfernt werden soll.

Would you like to remove Protractor from the project?

Klar, es macht für uns keinen Sinn, zwei E2E Tools in einem Projekt zu haben.

Daraufhin werden alle Projector Dateien gelöscht und Cypress Dateien angelegt.

DELETE e2e

Als nächsten kommt dann die automatische Konfiguration der CLI, um Cypress zu nutzen.

CREATE cypress.json (48 bytes)

CREATE cypress/tsconfig.json (196 bytes)

CREATE cypress/integration/spec.ts (123 bytes)

CREATE cypress/support/commands.ts (838 bytes)

CREATE cypress/support/index.ts (689 bytes)

UPDATE package.json (1401 bytes)

UPDATE angular.json (4333 bytes)

Was bedeutet das?

In der angular.json ist die Task e2e konfiguriert. Diese wird durch die @briebug/cypress-schematic umgeschrieben, um die Cypress-Funktionalität zu nutzen.

Zum Code

Das war’s auch schon mit der Vorbereitung. Es ist nur ein Befehl und die Bestätigung, dass man Protractor ersetzen möchte.

Der erste Test

Ausgeführt werden die Tests wie gewohnt (wenn man vorher schon mit Protractor gearbeitet hat).

ng e2e

Beim ersten Mal werden eine Ordnerstruktur und einige Config Dateien angelegt.

Abbildung - Angular-Testing - Ordnerstruktur und Config Dateien von Cypress

  1. fixtures: Hier können Mockdaten abgelegt werden, die dann im Test genutzt werden können.
  2. integration: Hier werden die Tests geschrieben.
  3. plugins: Hier können Plugins (wenn man diese benötigt) eingebunden werden.
  4. support: Hier können eigene Commands geschrieben oder existierende überschrieben werden.

Danach wird das Cypress UI gestartet.

Abbildung - Angular-Testing

Über den Button Run all specs kann man alle vorhandenen Testdateien und die darin geschriebenen Tests ausführen. In der Liste INTEGRATION TESTS sind alle Testdateien aufgeführt, und man hätte hier die Möglichkeit, einzelne Testdateien auszuführen.

Wir haben bisher nur eine Datei, aber man kann gut erahnen, wie einfach man seine Tests organisieren kann: Einfach in unterschiedliche Dateien auslagern.

Der Cypress Client

Wir klicken auf Run all specs und der Cypress Client wird gestartet. Der Client ist eine Electron App, die unsere Website lädt und die Tests ausführt.

Den Client kann man in drei Bereiche teilen. Bereich 1 ist der Reporter-Bereich, hier werden die Testergebnisse ausgegeben. Wenn man mit der Maus über die durchlaufenen Tests fährt, sieht man im Bereich 2 die Interaktionen, die in der App ausgeführt wurden. Im Bereich 3 findet man die Adressleiste und den Playground.

Abbildung - Angular-Testing - Der Cypress Client

Klickt man auf die Playground-Schaltfläche – das Fadenkreuz (1), wird der Playground eingeblendet. Hier hat man als Werkzeug den Selectorknopf (2). Einmal angeklickt, kann man nun in der App ein Element auswählen und bekommt einen Selektor (3) für dieses Element vorgeschlagen und kann den kompletten Aufruf direkt über die Copy-Schaltfläche (4) in den Zwischenspeicher legen.

Hat man den Selectorknopf geklickt, bekommt man beim Überfahren von Elementen in der App auch Hinweise zu den Elementen (5).

Initial sind noch keine sinnigen Tests vorhanden, nur ein “must Failing” Test, da der erwartete Text ‘Replace me with something relevant’ nicht gefunden wird.

Abbildung Angular-Testing - “must Failing” Test

Zum Code

Aber wir können hier schon etwas Wichtiges sehen.

Alle Tests müssen in einem it() geschachtelt sein. Das it() hat keine echte Bedeutung, es geht hier darum, Tests zu schreiben, die für den Entwickler lesbar sind.

Wir können die Tests auch noch weiter organisieren. Zum Beispiel können wir Test Suits definieren, um darin dann die Tests zu schreiben.

Eine Test Suit wird über describe() definiert:

describe('Next Steps', () => {...})

Innerhalb eines describe kann ich beliebig viele it() oder auch weiter describe definieren, um meine Tests zu organisieren.

Außerdem gibt mir describe die Möglichkeit, mit Preflies oder Rollbacks zu arbeiten.

Hier ein Beispiel:

Abbildung - Angular-Testing Preflies oder Rollbacks

Zum Code

Das it() wie auch das describe() haben zwei Parameter. Der erste ist ein String und wird im Testergebnis als Titel ausgegeben. So ist das Testergebnis nachher gut lesbar und aussagekräftig.

Der zweite Parameter ist dann unser Test.

Zur Erinnerung, E2E Tests sind wie ein User Test. Alles was der Nutzer tut, kann ich auch im Test tun.

Was möchte ich testen? Was sollte ich testen?

Testen sollte man:

  • Alles, was in den User Stories steht. Die Expectations sind geradezu perfekt als Test Titel (erster Parameter eines it) geeignet.
  • Alles, was einem kritisch erscheint. Alles, was beim Entwickeln etwas mehr Hirnschmalz erfordert hat, oder was einem unheimlich vorkam.
  • Alles, was schon einmal kaputt war. Es gibt nichts Ärgerlicheres, als einen Fehler später nochmal zu finden.

Was ich hier testen möchte:

  • Beim Besuchen der Seite soll der Nutzer mit dem Namen des Projekts begrüßt werden, in dieser Art: ProjektName app is running.
  • Beim Klick auf eine der Next Steps soll in der Anzeige (sieht aus wie eine Console) eine bestimmte Ausgabe angezeigt werden.

CY Methoden

In den Tests hat man Zugriff auf das globale Cypress Objekt (cy). Ueber cy habe ich Zugriff auf viele Methoden, wir schauen uns einige davon an. Die erste Methode ist mit die wichtigste.

Das globale Cypress Objekt gibt Zugriff auf Methoden, die ich zum Testen nutzen kann:

  • .visit(url): Damit kann ich im Browser (im Cypress Client) eine bestimmte URL aufrufen.In unserem Beispiel wollen wir die Standard URL unseres Angular Projektes aufrufen. cy.visit(„http://localhost:4200„); Der User besucht unsere Website.
  • .get(selector): Hiermit kann ich Elemente auswählen, um Aktien oder Prüfungen darauf auszuführen. Der Selektor ist dabei der Standard CSS Selektor.
  • .contains(content): Damit kann geprüft werden, ob ein Text/Content gefunden wurde. Es kann hiermit aber auch gefiltert werden. (Das zeige ich gleich noch.)
  • .should(chainer, value): Mit dieser Methode können Prüfungen durchgeführt werden. Der Chainer ist hierbei die Erwartung (Expectation), die gegen den Wert (Value) geprüft wird. (Auch das sehen wir gleich).

Unser erster wirklicher Test:

In der app.component.html finden wir Folgendes.

<span>{{ title }} app is running!</span>

Wir wollen prüfen, ob cypress-test app is running! in einem Span ausgegeben wird.

Abbildung - Angular-Testing cypress-test app is running!

Zum Code

Okay, was passiert hier?

  1. Wir haben einen Test Case a definiert mit dem Titel unseres ersten Tests.
  2. Wir rufen die Startseite der App auf.
  3. Wir suchen nach einem Span, welches unseren erwarteten Text beinhaltet.

So einfach kann es sein.

Sollte es mal nicht so einfach sein, Elemente zu finden, gibt uns der Cypress Client ja den Selectorknopf als Hilfsmittel.

Wir sehen im Reporter-Bereich, dass der Test grün ist. Wenn man nun mit der Maus über die Zeile 2 des Tests fährt, sieht man in der App alle Spans, die gefunden wurden.

Würde es keinen Span mit dem gesuchten Text geben, gäbe es einen Timeout und einen Fehler.

Interagieren mit Elementen

Jedes Element, welches über .get() gefunden wurde, kann Aktionen empfangen. Dabei handelt es sich um die üblichen Aktionen, die ein Nutzer ausführen kann (z.B. klicken oder Text eingeben).

Hier ist eine Liste von Aktionen:

Wir wollen nun also den zweiten Test schreiben.

Beim Klick auf eine der Next Steps soll in der Anzeige (sieht aus wie eine Konsole) eine bestimmte Ausgabe angezeigt werden.

Also legen wir erstmal ein neues it() mit dem Titel an:

Abbildung - Angular Testing - neues it()

Zum Code

Nun wollen wir alle “Next Steps”-Schaltflächen testen.

Ich habe mir ein kleines Objekt für alle “Next Steps” und den dazugehörigen Meldungen gebaut.

Abbildung - Angular-Testing - kleines Objekt für alle “Next Steps”

Zum Code

Ich möchte jetzt über dieses Objekt iterieren und in jeder Iteration einen Test ausführen.

Abbildung - Angular-Testing - in jeder Iteration einen Test ausführen

Zum Code

Damit habe ich schon mal sichergestellt, dass die Schaltflächen existieren. Nun muss ich noch prüfen, ob der erwartete Text angezeigt wird.

Dafür müssen wird uns das Element holen und auf den Inhalt prüfen.

cy.get('div.terminal').should('contain', commands[step]);

Damit hätten wir auch diesen Test durchgeführt.

Hier mal das komplette it():

Abbildung - Angular-Testing - komplette it()

Zum Code

Alias

Mit Cypress haben wir die Möglichkeit, Elemente als Alias abzulegen, um auf diese mehrfach zugreifen zu können. Mit der methode .as(‘myAlias’) legen wir den Alias an und können auf diesen dann über die Methode .get(‘@myAlias’) zugreifen. Das vorangestellte @ gibt Cypress hier den entscheidenden Hinweis, dass es sich hierbei um einen Alias handelt.

Das würde man in einem beforeEach() tun.

Abbildung - Angular-Testing - beforeEach()

Zum Code

Diesen Alias können wir nun in unserem zuletzt geschriebenen Test nutzen.

Abbildung - Codebeispiel Angular-Testing

Zum Code

Unsere App ist ja sehr limitiert, aber in einem echten Projekt hätte man Formulare, die man ausfüllen und abschicken würde.

Ein Test könnte so aussehen:

Abbildung - Angular-Testing Beispiel

Zum Code

HTTP-Aufrufe

Wir können auch prüfen, ob das Absenden der Daten funktioniert. Dafür müssen wir das Feature aktivieren, das in dem Servermodule enthalten ist.

Dieses Module bringt die Möglichkeit, alle HTTP-Kommunikation zu überwachen, zu manipulieren oder zu beantworten.

Das bedeutet, wir brauchen kein Backend ansprechen.

Zuerst muss das Servermodul gestartet werden, dies passiert auch im beforEach().

cy.server();

Ich habe noch die Möglichkeit, diverse Konfigurationen an den Server zu übergeben. Diese Information finden Sie in der Dokumentation.

Wenn wir nun einen Endpunkt “wegmocken” wollen, ist das sehr einfach. Dafür wird eine Route definiert:

Abbildung - Angular-Testing - Endpunkt “wegmocken”

Zum Code

Wir sehen, dass diese API sehr variabel ist.

Nachstehend ein Beispiel:

cy.route('http://my.api.com', { name: 'Hannes' });

Solange die Response so schön klein ist, ist es auch okay (Achtung, persönliche Meinung!) diese direkt im Mock zu schreiben. Aber das entspricht ja sehr selten dem echten Entwicklerleben. Dafür bietet Cypress die Möglichkeit, gemockte Daten in Form von Dateien als Response zu nutzen.

Dafür ist der Ordner cypress\fixtures gedacht. Hier kann ich Dateien ablegen, die ich dann als Response nutzen kann. Hier liegt eine example.json. Wenn wir diese als Response nutzen wollen, müssen wir im Mock statt dem Objekt { name: 'Hannes' }  eine (seltsam aussehende) Cypress Syntax schreiben. Nämlich das Keyword fixture: gefolgt vom Namen der Datei.

cy.route('http://my.api.com', 'fixture:example.json');

Natürlich ist jedem sofort aufgefallen, dass ich vom Absenden der Daten gesprochen habe. Wenn ich keine HTTP-Methode an .route() übergebe, ist es per Default ein GET.

Ein POST Mock würde so aussehen:

Abbildung - Angular-Testing POST Mock

Zum Code

Alle POSTs auf den User Endpoint sind über den Alias @new-user erreichbar und können wie folgt getestet werden.

Abbildung - Angular-Testing - Alias @new-user

Zum Code

Fazit

Sie sehen, Cypress bietet eine Menge Möglichkeiten und macht dabei auch noch Spass. Wir haben gelernt, wie man Elemente findet und welche Prüfungen wir darauf ausführen können. WIr haben die verschiedenen Methoden kennengelernt, wie wir mit DOM Elementen interagieren können. Und wir habe einen Überblick über die Möglichkeiten von Cypress bekommen.

Ich sehe anhand der Teilnehmer meiner Workshops, dass der Einstieg in Cypress sehr einfach ist und man nach kurzer Zeit eine gute Testabdeckung erreicht.

Gratis E-Book Angular zum Download

Weitere Experten-Beiträge zu Angular finden Sie in unserem E-Book. Jetzt kostenlos downloaden.

 

David Müllerchen
Letzte Artikel von David Müllerchen (Alle anzeigen)

Große Auswahl an günstigen Domain-Endungen – schon ab 0,08 € /Monat
Jetzt Domain-Check starten