Visual Testing von Webanwendungen

Permalink

Aufmerksam gemacht auf BackstopJS wurde ich durch einen Artikel in der c’t. Da ich sowieso gerade eine Aktualisierung meiner kleinen Web Component zur Bildauswahl vornehmen wollte habe ich die Anregung gerne aufgegriffen.

Mit BackstopJS kann man Screenshots einer Webseite machen um diese mit einer Referenz zu vergleichen. Sollten einzelne Pixel voneinander Abweichen bemerkt das Tool dies und schlägt Alarm.

Installation

Die Installation ist über NPM einfach durchgeführt.

npm install --save-dev backstopjs

Ich gehe dabei davon aus das es sich um ein Projekt handelt dessen Dependency Management mit NPM gemacht wird. Anderenfalls müsste man BackstopJS global installieren. Anschließend kann man das Tool für sein eigenes Projekt initialisieren. Dabei wird eine Konfigurationsdatei backstop.json erzeugt.

npx backstop init

Grundsätzlich wäre Backstop jetzt schon einsetzbar. Allerdings simuliert das Tool einen Browser. Damit wird auch ein Server benötigt von dem BackstopJS die Anwendung abrufen kann. Je nach dem um was es geht gibt es verschiedene Applikationsserver die man nutzen kann. Ich brauche für meinen Bedarf nur einen einfachen Webserver. Ich habe mich dabei ohne besonderen Grund für http-server entschieden.

npm install --save-dev http-server

Um später sowohl den Server als auch BackstopJS automatisiert nutzen zu können setze ich auch noch Concurrently ein. Dazu weiter unten mehr.

npm install --save-dev concurrently

Konfiguration

Die zuvor erstellte Konfigurationsdatei enthält drei Bereiche: Viewports, Szenarien und Einstellungen.

Viewports

Mit Viewports kann man die Maße des Browserfensters festlegen wenn die Screenshots durch BackstopJS angefertigt werden. Dabei sind mehrere Größenangaben möglich. Die Testfälle werden dann für jede dieser Größen durchgeführt. So kann man Tests für verschiedene Displaygrößen, z.B. von Smartphones und Desktops, durchführen.

Szenarien

Mit Szenarien beschreibt man die einzelnen Testfälle. Jedes Szenario wird primär durch einen Namen (label) und zwei URLs beschrieben. Die Referenz-URL (referenceUrl) wird aufgerufen wenn BackstopJS die Screenshots mit dem Soll-Zustand erzeugt. Damit werden dann Screenshots verglichen die von der Seite hinter der Test-URL (url) angefertigt werden.

Durch die zwei verschiedenen URLs lässt sich das Referenzmaterial z.B. von einem produktiven System anfertigen. Die Test-Screenshots möchte ich natürlich vom Entwicklungsstand und damit von http-server beziehen.

Wenn auf der Testseite Dinge sind die nicht relevant für den Test sind kann ich mittels der Angabe selectors eine Liste von CSS-Selektoren angeben. Anschließend beschränkt sich der jeweilige Screenshot auf die Elemente die diesen Selektoren entsprechen.

Wenn es darum geht dynamische Inhalte zu testen muss ich irgendwie die Interaktion des Nutzers simulieren. Um das zu ermöglichen setze Backstop auf Puppeteer. Puppeteer zu erklären würde den Artikel sprengen. Nur soviel: Mit onReadyScript lässt sich ein Script angeben das ausgeführt wird sobald die Testseite geladen wurde.

Einstellungen

Alles was nicht Viewport oder Szenario ist fasse ich mal unter Einstellungen zusammen. Hauptsächlich geht es um Speicherorte für die Screenshots und die Testergebnisses und -berichte. Dazu kommen Parameter für den Browser und Puppeteer.

Eine Wert der später noch wichtig wird ist die Angabe der Art der Ausgabe des Testergebnisses. Mit report lässt sich festlegen ob ein menschen lesbarer HTML-Report eine JUnit XML- oder eine JSON-Datei (oder alles gleichzeitig) erzeugt werden soll.

Nutzung

Für die folgenden Schritte braucht man eine aufrufbare Fassung der zu testenden Webseite. Genau dafür habe ich vorher http-server installiert. In einer Shell kann ich den Server mit npx http-server -p 3001 -c-1 starten. In einer zweiten Shell kann ich dann die unten stehenden Befehle ausführen.

Das Arbeiten mit BackstopJS vollzieht sich in drei Phasen. Alles beginnt mit dem anfertigen der Referenzbilder: Von einer validen Fassung der zu testenden Webseite oder -app.

npx backstop reference

Nach der Entwicklung einer neuen Version folgt dann der Test. Erneut werden Screenshots erzeugt, mit den Referenzbildern verglichen und ein Report erzeugt.

npx backstop test

Anhand des Reports geht es dann an die Bewertung der Abweichungen. Solange etwas nicht zufriedenstellend ist wiederholt sich der Zyklus aus Entwicklung und Test. Sobald ich nichts mehr zu bemängeln habe wird die dritte Phase abgeschlossen indem die Referenzen aktualisiert werden.

npx backstop approve

Ausflug: Git LFS

Wie ich eben ausgeführt habe werden des öfteren Screenshots angefertigt. Da diese als Dateien gespeichert werden muss ich im Zusammenspiel mit Git ein paar Besonderheiten berücksichtigen. Git ist nicht besonderst gut darin Binärdaten zu managen. Damit das nicht zum Problem wird gibt es Git LFS. Damit werden Binärdateien im Git-Repository durch Pointer ersetzt während die Dateien außerhalb abgelegt werden.

Zuerst muss ich es für meine Git-Benutzereinstellungen herrichten, etwas das auf jedem Endgerät wiederholt werden muss:

git lfs install

Anschließend muss ich dem Git-Repository noch mitteilen für welche Dateien LFS zuständig sein soll. Im vorliegenden Fall wären das alle Bilddateien aus dem Ordner mit den Referenzbildern.

git lfs track "backstop_data/bitmaps_reference/*"

Der Befehl führt zu einer Eintragung in der Datei .gitattributes. Diese habe ich sinnvollerweise auch eingecheckt. Im Gegensatz dazu brauche ich weder die Screenshots aus den Tests noch die Reports in meinem Repository. Deshalb gehören die drei entsprechenden Ordner auf die Ignore-List:

backstop_data/bitmaps_test/
backstop_data/html_report/
backstop_data/ci_report/

Automatisation

Testen wird schnell lästig darum automatisiere ich alles wenn möglich. In meinem Fall besteht die Lösung aus einer Kombination aus NPM-Scripting und GitHub Actions.

NPM

Das folgende Script, in die Datei package.json eingefügt startet den Test:

"test:visual": "concurrently --success=first --kill-others \"npx http-server -p 3001 -c-1\" \"npx backstop test\"",

Der erste Teil (concurrently) führt mehrere Befehle parallel aus. Dabei werden alle parallelen Prozesse abgebrochen (--kill-others) sobald einer davon beendet ist (--success=first).

Im ersten der parallelen Befehle (http-server) wird der Webserver gestartet. Der zweite Befehl führt den eigentlichen Test aus.

GitHub Action

Dieses Script kann jetzt in einer Action ausgeführt werden. Dabei lauern aber zwei Fallstricke:

Ich muss beim Checkout innerhalb der Action LFS aktivieren.

- uses: actions/checkout@v1
  with:
     lfs: true

Außerdem sind die Referenzbilder vom Entwicklungsrechner nicht unbedingt passend für die Laufzeitumgebung innerhalb der Action. Ich entwickle auf Windows und die Actions laufen üblicherweise mit Ubuntu. Das führt zu leichten Unterschieden im Rendering und damit auch in den Screenshots. Ich umgehe das Problem einfach indem ich auch die Referenzbilder immer neu erstelle, natürlich nicht von der neu entwickelten Version des Projektes sondern von der produktiven.

Abschlussbemerkung

Das Projekt das ich zum ausprobieren verwendet habe kann auf GitHub eingesehen werden. Um es etwas komplizierter zu machen verwende ich dabei abweichend von oben aber eine Javascript-Konfiguration .backstoprc.js anstelle der JSON-Datei.

Weitere Artikel zu