Unser Newsletter rund um technische Themen,
das Unternehmen und eine Karriere bei uns.

9 Minuten Lesezeit (1700 Worte)

Custom Commands mit Cypress

In unserem letzten Blog-Artikel zum Thema Cypress haben wir eine Beispielanwendung gebaut und zu dieser passende Cypress Tests geschrieben. Nun wollen wir untersuchen, wie auch komplexere Anwendungen mit Cypress getestet werden können. Dazu schauen wir uns ein Werkzeug an um lesbareren, wiederverwendbaren und performanten Code zu schreiben: Custom Commands/Benutzerdefinierte Befehle.

Befehle in Cypress

Cypress stellt standardmäßig einige Befehle zum Testen, wie zum Beispiel visit() und click(), bereit. Diese Befehle werden mit Parametern aufgerufen. So wird bei visit() beispielsweise eine URL übergeben, die visit daraufhin besucht. Zudem können Befehle verkettet werden. Eine Erläuterung der Befehls-Ketten folgt im Abschnitt „Arten von Befehlen".

Einen Einstieg in das Arbeiten mit Cypress Befehlen bietet unser letzter Blogartikel zum Thema Cypress "End-to-End-Test in Angular mit Cypress", anhand einer Beispielanwendung.

Cypress erlaubt uns darüber hinaus, benutzerdefinierte Befehle zu schreiben. Diese Befehle können mit Parametern arbeiten, verkettet werden und Dokumentation erhalten; genauso wie alle Standardbefehle von Cypress. Es können sogar bereits existierende Befehle erweitert oder vollständig überschrieben werden.

Die Beispielanwendung

Unsere Beispielanwendung aus dem letzten Blogartikel besteht nur aus einem Knopf und einem Zähler, der sich bei jedem Knopfdruck um eins erhöht hat. Wir erweitern sie nun um ein Textfeld. In das Textfeld gibt der Anwender eine Zahl ein, die auf Knopfdruck auf unseren Zähler addiert wird. Als Standardwert verwenden wir eine Eins.

layout.component.html

<p id="counter" class="counter">{{counter}}</p>
<mat-form-field appearance="fill" class="text-field">
    <textarea matInput id="text-field" placeholder="1"
              [formControl]="textFormControl"></textarea>
    <mat-hint align="end">Zahl eingeben</mat-hint>
</mat-form-field><br/>
<button mat-raised-button id="count-button" color="primary" (click)="clickButton()">Klick mich!</button>
 

layout.component.ts

import {Component} from '@angular/core';
import {FormControl} from '@angular/forms';

@Component({
  selector: 'ox-layout',
  templateUrl: './layout.component.html',
  styleUrls: ['./layout.component.scss']
})
export class LayoutComponent {
  public counter: number = 0;
  public textFormControl = new FormControl('');

  public clickButton(): void {
    let number = parseInt(this.textFormControl.value);
    if (!number) {
      number = 1;
    }
    this.counter += number;
  }
}
 

Jetzt, wo wir die Anwendung angepasst haben, können wir sehen, wie benutzerdefinierte Befehle das Testen dieser Anwendung erleichtern.

Eigene Befehle hinzufügen

Im Projekt befindet sich im Cypress-Ordner ein Unterordner support. In diesem Ordner liegt die Datei commands.js, in der wir nun unseren ersten benutzerdefinierten Befehl schreiben. Der Befehl bekommt ein Zahlenarray als Parameter übergeben und trägt jede Zahl aus dem Array in das Textfeld ein. In der Datei rufen wir zunächst Cypress.Commands.add() auf und übergeben der Methode zwei Parameter. Der erste ist der Name unseres benutzerdefinierten Befehls. Für ihn geben wir enterNumbers an. Der zweite Parameter ist die Funktion, die aufgerufen werden soll, wenn unser Befehl ausgeführt wird. In der Funktion verwenden wir eine for-Schleife, um für jede Zahl das Textfeld zu leeren, die Zahl einzugeben und den Knopf zu drücken.

Cypress.Commands.add('enterNumbers', (numbers) => {
  for (let number of numbers) { 
    if (!number) {
      number = 0;
    }

    cy.get('[id=text-field]').clear().type(number);
    cy.get('[id=count-button]').click();
  }
});
 
Nun können wir mit cy.enterNumbers() unseren Befehl ausführen oder ihn an eine Befehls-Kette anhängen.

Bevor wir den Befehl in einem Test verwenden, fügen wir noch einen zweiten Befehl hinzu. Unseren zweiten Befehl nennen wir repeatClicks; er soll mehrfach auf ein Element klicken. Dazu beginnen wir unseren Code wieder mit Cypress.Commands.add() und dem Namen des Befehls als ersten Parameter.

Diesmal übergeben wir als dritten Parameter unsere Funktion. Als zweiten Parameter übergeben wir die Option prevSubject mit dem Wert element. Durch die Angabe dieser Option erhält unsere Funktion als ersten Parameter das vorherige Element der Befehls-Kette.

Diesen Funktions-Parameter nennen wir subject und fügen noch den zweiten Parameter amount hinzu, der unserer Funktion angibt, wie oft sie klicken soll. In der Funktion überprüfen wir, dass amount einen validen Wert hat (z.B. nicht NaN) und führen in einer for-Schleife safeClick() aus. Dies ist ebenfalls ein benutzerdefinierter Befehl, der standardmäßig in der command.js Datei von Cypress generiert wird. Er dient beim Erstellen von eigenen Befehlen als Vorbild.

Wie alle anderen benutzerdefinierten Befehle können wir safeClick() problemlos auch in der Definition eines anderen benutzerdefinierten Befehls aufrufen. Der Unterschied zum normalen click() ist, dass gewartet wird, bis die Webanwendung meldet, dass alle Veränderungen auf der Seite abgeschlossen sind und dann erst der click() ausgeführt wird.

Cypress.Commands.add('repeatClicks', {prevSubject: 'element'}, (subject, amount) => {
  if (!amount) {
    amount = 0;
  }
  for (let i = 0; i < amount; i++) {
    cy.wrap(subject).safeClick();
  }
});
 

Arten von Befehlen

Cypress-Befehle können als Ketten geschrieben werden. Der Anfang einer Kette wird durch cy signalisiert und das Ende durch ein Semikolon. Auf das cy folgt ein Punkt und der erste Befehl der Kette. Anschließend können beliebig viele weitere Befehle an die Kette angehängt werden. Eine Kette könnte also in etwa so aussehen: cy.visit().get().click();

Unser Befehl enterNumbers() kann mit cy.enterNumbers() am Anfang einer Kette stehen oder später in einer Kette aufgerufen werden. Im zweiten Fall ignoriert er jedoch die vorherige Ausgabe aus der Kette und startet intern eine neue Kette. Befehle wie dieser, die immer am Anfang einer Kette stehen werden Eltern-Befehle genannt.

Unser Befehl repeatClicks() nimmt das vorherige Element der Kette entgegen und führt Aktionen basierend auf diesem Element aus. Dieser Befehl kann nicht der erste Befehl in einer Kette sein, da ihm so kein Element übergeben werden kann. Wird er dennoch als erster Befehl in eine Kette gestellt zeigt Cypress an der Stelle einen Fehler. Solche Befehle werden Kind-Befehle genannt.

Die dritte und letzte Art von Befehlen, die es in Cypress gibt, sind Doppel-Befehle. Diese Befehle kombinieren Eltern- und Kind-Befehle. Sie können am Anfang einer Kette stehen, um diese zu starten, können aber auch weiter hinten in einer Kette aufgerufen werden und führen diese problemlos weiter. In der Praxis werden Doppel-Befehle eher selten benutzt und werden in diesem Artikel deswegen auch nicht weiter untersucht. Auch unter den von Cypress bereitgestellten Befehlen gibt es nur eine Handvoll Doppel-Befehle.

Befehle überschreiben

Es ist ebenfalls möglich, bereits existierende Befehle zu erweitern oder ganz zu überschreiben. Dazu muss Cypress.Commands.overwrite() aufgerufen werden. Genau wie beim Erstellen eines eigenen Befehls, wird als erster Parameter der Name des Befehls übergeben und als zweiter Parameter die Funktion, die aufgerufen werden soll. Diese Funktion muss einen Parameter mehr haben als die ursprüngliche Funktion, weil die ursprüngliche Funktion als erster Parameter mitgegeben wird.

So kann der Entwickler beispielsweise kleine Änderungen an den Optionen vornehmen und anschließend die Ursprungsfunktion mit den angepassten Optionen aufrufen. Dieser Weg eignet sich um Standardwerte in den Befehlen festzulegen. Es ist darauf zu achten, dass die Funktion einen Rückgabewert benötigt. Oft eignet sich als Rückgabewert der Funktionsaufruf der Original-Funktion.

Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
  if (!url || url === '') {
    url = '/start-page'
  }
  return originalFn(url, options)
});
 

Tests schreiben

Die Verwendung benutzerdefinierter Befehle ist unkompliziert. Sie werden wie die normalen Befehle verwendet.

it('[01-03] enter number and click button', function () {
    cy.visit('/')
        .get('[id=counter]').should('contain', '0');
    cy.enterNumbers([5])
        .get('[id=counter]').should('contain', '5');
});

it('[01-04] enter multiple numbers and click button each time', function () {
    cy.visit('/')
        .get('[id=counter]').should('contain', '0');
    cy.enterNumbers([5, 0, 7, NaN, 3, -2])
        .get('[id=counter]').should('contain', '13');
});

it('[01-05] enter number and click button multiple times', function () {
    cy.visit('/')
        .get('[id=counter]').should('contain', '0')
        .get('[id=count-button]').repeatClicks(5)
        .get('[id=counter]').should('contain', '5');
});
 

Dokumentation hinzufügen

Die Befehle, die wir in die Datei commands.js geschrieben haben, verfügen noch über keine Dokumentation. Auch andere Details wie Typangaben für die Parameter und den Rückgabewert fehlen noch. Wer eine IDE mit automatischer Syntax Erkennung nutzt stellt fest, dass die von uns definierten Befehle nicht als Befehle erkannt werden.


Diese Punkte können wir alle beheben, indem wir unsere Befehle in der Datei index.d.ts im support-Unterordner dokumentieren. In der Datei gibt es eine von Cypress automatisch generierte Struktur, die wir beibehalten müssen. Wenn wir uns daran halten, können wir alle Befehle, die wir geschrieben haben, mit deren Namen definieren, Typen für Parameter und den Rückgabewert festlegen und eine Dokumentation schreiben, die unsere Befehle erläutert und von einigen IDEs erkannt und zu dem Befehl angezeigt wird. Es ist jedoch zu beachten, dass bei Kind-Befehlen das vorherige Element nicht als Parameter aufgeführt wird. Zudem ist der Rückgabewert für jeden Befehl, der verkettet werden kann Chainable <Element>.

declare namespace Cypress {
  interface Chainable {
    /**
     * Enters each number into the text field and clicks the button.
     */
    enterNumbers(numbers: number[]): Chainable<Element>;

    /**
     * Uses safeClick to click the chained element repeatedly.
     * @param amount Number of times the element should be clicked.
     */
    repeatClicks(amount: number): Chainable<Element>;
  }

}
 

Cypress logs anpassen

Bei Befehlen, die sehr viele andere Befehle aufrufen, kann es dazu kommen, dass die Cypress_Logs sehr unübersichtlich werden und das Debuggen erschweren. In so einem Fall ist es sinnvoll, das Logging für einen benutzerdefinierten Befehl abzuändern. Dazu wird jedem Befehl die Option {log: false} übergeben und eine eigene log-Ausgabe mit Cypress.log() definiert.

Cypress.Commands.add('enterNumbers', (numbers) => {
  for (let number of numbers) {
    cy.get('[id=text-field]').clear({log: false}).type(number, {log: false});
    cy.get('[id=count-button]').click({log: false});
  }
  Cypress.log({name: 'enterNumbers: ', message: numbers});
});
 

Welche Möglichkeiten es gibt diese benutzerdefinierten Logs übersichtlich zu gestalten kann in der offiziellen Cypress Dokumentation nachgelesen werden.

Best Practices

Benutzerdefinierte Befehle sind ein Werkzeug, um Cypress-Code leserlicher zu gestalten und das Arbeiten mit ihm zu erleichtern. Folgende Regeln sind hilfreich um diese Ziele zu erreichen:

  • Nur einen neuen Befehl erstellen, wenn es in mehreren Tests hilft. Jeder neue Befehl muss erst verstanden werden. Findet der Befehl dann nur ein einziges Mal Anwendung ist er den Aufwand wahrscheinlich nicht wert.
  • Nicht aus allem einen eigenen Befehl machen. Befehle wie getButton() sind überflüssig.
  • Nicht die Code-Struktur von Cypress Tests unterbrechen. Cypress wurde so designt, dass der Code wie ein Satz gelesen werden kann. Neue Befehle sollten sich in dieses Schema einfügen.
  • Die Befehle nicht zu komplex werden lassen.
  • Der Name des Befehls sollte so gewählt werden, dass sein Name die generelle Funktionsweise beschreibt.

Last but not least: Die Benutzeroberfläche umgehen. Befehle, die immer wieder aufgerufen werden, sollten schnell sein damit die Testdauer möglichst kurz bleibt. Eine REST-Schnittstelle anzusprechen ist zum Beispiel schneller, als ein Formular auf einer Webseite auszufüllen, kann aber genauso gut genutzt werden um Testdaten anzulegen.

Fazit

Benutzerdefinierte Befehle sind ein sehr nützliches Werkzeug um das Testen mit Cypress zu erleichtern und den Code übersichtlicher zu gestalten. Dabei wird dem/der Programmierer/in viel Freiraum gelassen verschiedene Arten von Befehlen zu erstellen oder zu überschreiben und mit passender Dokumentation und Logging nach den eigenen Präferenzen zu gestalten.

Junior Consultant bei ORDIX.

 

Kommentare

Derzeit gibt es keine Kommentare. Schreibe den ersten Kommentar!
Donnerstag, 26. Dezember 2024

Sicherheitscode (Captcha)

×
Informiert bleiben!

Bei Updates im Blog, informieren wir per E-Mail.

Weitere Artikel in der Kategorie