Automatisiertes Testing von Screenshots mit Sitegeist.Monocle & BackstopJS
Um solche Probleme zu vermeiden, werden Neos Frontends bei sitegeist seit einigen Jahren mit unserem Styleguide Sitegeist.Monocle entwickelt und getestet und die Styles der Komponenten werden durch CSS Modules strikt voneinander getrennt gehalten. Zusammen mit unserem vier Augen Prinzip und mehrerer TestStages konnten wir die Qualität der Ergebnisse deutlich steigern. Leider sind uns aber immer noch gelegentlich Änderungen durchgerutscht - insbesondere wenn diese nicht offensichtliche Änderungen an anderer Stelle zur Folge hatten.
Die Lösung, immer alles zu testen, ist nicht nur unbefriedigend, sondern wird auch dadurch erschwert, dass nicht jeder Kollege visuelle Abweichungen gleich empfindet. Farben, Anstände und Schriften sind für manche sehr subtil. Der alte Witz, dass Backend-Entwickler nur 16 Farben kennen, hat insofern einen wahren Kern.
Wenn das Erkennen von visuellen Abweichungen aber nicht einfach ist, bietet sich natürlich eine Objektive Messung der Unterschiede an. Schließlich sind wir Techniker. Hierbei wird ein Screenshot genommen und mit einem Referenzbild verglichen. Ganz trivial könnte nun die Anzahl der veränderten Pixel gemessen werden und Alarm geschlagen werden wenn diese einen Grenzwert übersteigt.
Da CMS Seiten redaktionellen Änderungen unterliegen, kann das natürlich nicht mit der Webseite selbst gemacht werden, da bereits ein geänderter Text oder ein angepasstes Bild zu Fehlermeldungen führen würden. Anders sieht es mit dem Monocle Styleguide aus. Dieser verwaltet ja bereits alle Bausteine der Webseite und hat auch bereits Dummy-Daten zur redaktionsunabhängigen Darstellung.
Seit der Entwicklung von Monocle haben wir dieses Ziel verfolgt. Allerdings waren noch einige technische Hürden zu nehmen, bevor wir vollständig automatisiert unsere Komponentenbibliotheken testen konnten. Im folgenden wird der dazugehörige Technologie Stack und die wichtigsten Learnings beschrieben:
Testing Tool - BackstopJS
Zum testen und vergleichen von Screenshots nutzen wir das Tool BackstopJS welches es erlaubt eine Reihe von Scenarios basierend aug URLS zu definieren. Backstop ruft alle definierten Szenarien auf nimmt einen Screenshot und vergleicht diesen mit Referenzbildern.
Um BackstopJS mit dem Monocle Styleguide zu verbinden, wurde ein Zusatzpaket Sitegeist.Monocle.BackstopJS erstellt, welches eine BackstopJS Konfigurationsdatei erstellt, die Szenarios für alle Elemente eines Styleguides in allen Breakpoints enthält.
# Installation
yarn add --dev backstopjs
composer require sitegeist/monocle-backstopjs
# Erzeugen einer Konfiguration und tests ausführen
./flow backstop:configuration --package-key Vendor.Site --base-uri http://127.0.0.1:8080 > backstop.json'
backstop test --config=backstop.json'
Konsistentes Font Rendering
Leider gibt es auf unterschiedlichen Betriebssystemen relativ große Abweichungen beim Rendern von Schriften, sodass BackstopJS Fehler melden wird, wenn die Tests auf unterschiedlichen Betriebssystemen ausgeführt werden.
Glücklicherweise nutzen wir sein einiger Zeit Docker in Gestalt von DDEV als Basis unserer Entwicklungs-Setups und haben somit eine definierte Linux Umgebung bei jedem Entwickler. Lediglich das chromium Paket musste zusätzlich in der .ddev/config.yaml eingetragen werden.
# .ddev/config.yaml
webimage_extra_packages: [chromium]
Image Lazy Loading
Moderne Webseiten laden viele Bilder erst nach, wenn der Besucher tatsächlich in die Nähe der Bilder scrollt. Damit kann viel Bandbreite und Ladezeit gespart werden.
Leider ist dieses Verhalten in automatisierten Screenshot Tests nicht hilfreich. Einerseits werden dabei oft Bilder von leeren Bildbereichen aufgezeichnet. Aber weit problematischer ist, dass das Nachladen nicht 100% deterministisch ist und Screenshots abwechselnd mit oder ohne geladenen Bildern erstellt werden. Was natürlich wiederum zu Fehlermeldungen von BackstopJS führt.
Unsere Lösung besteht in der Verwendung eines speziellen Flow Contextes, in welchem die Lazyness und andere Funktionen, die das Testen stören, standardmäßig deaktiviert werden. Somit kann vermieden werden, dass im Visual Regression Testing auf das Laden der Bilder gewartet wird und die Tests laufen mit der maximal möglichen Geschwindigkeit.
FLOW_CONTEXT=Development/VisualRegressionTesting ./flow server:run --port 8080
Es gibt alternativ andere Ansätze für LazyLoading in VisualRegression Testing. So kann beispielsweise beim Start von BackstopJS JavaScriptCode integriert werden, der das Laden der Bilder auslöst. Der Ansatz über einen speziellen Flow Context erlaubt uns allerdings auch anderen Funktionen während des Tests speziell zu konfigurieren. So vermeiden wir beispielsweise Datenbankzugriffe während der Tests, indem wir einige Funktionen wie die redaktionelle Pflege von Übersetzungen in diesem Context deaktivieren.
Continuous Integration in Gitlab CI
Die letzte Herausforderung für war die Integration in gitlab CI. Hier galt es einerseits, die Screenshots parallel zum den Bereits vorhandenen Code-Lintern auszuführen. Andererseits wollten wir vermeiden, dass die Applikation in jedem Schritt erneut `yarn` und `composer install` sowie `yarn build` ausführen muss. Des weiteren sollte das Testing parallel zu den vorhandenen Test laufen, damit die Entwickler zeitnah Feedback bekommen und Änderungen nicht verzögert werden.
Unsere Lösung basiert auf den Build Artifacts von GitlabCi. Im Kern übernimmt eine `build` stage das installieren der Webseite, das Bauen des JS und CSS sowie das Anwärmen des Flow-Caches. Die folgenden Stages `test` und `deploy` können dann mit einem Fertig installierten System starten und wo sinnvoll parallel arbeiten.
Damit die BackstopJS tests innerhalb des Gitlab Runners ausgeführt werden können, mussten wir einen speziellen Docker-Container mit dem chromium Paket bereitstellen.
# .gitlab-ci.yml
stages:
- build
- test
build:
stage: build
artifacts:
name: "Flow Distribution"
exclude:
- '.git/**/*'
- '.git'
paths:
- './'
expire_in: 1 hrs
variables:
FLOW_CONTEXT: Development/VisualRegressionTesting
script:
- 'composer install --no-progress --quiet'
- 'yarn --silent'
- 'yarn build'
- './flow flow:cache:warmup'
- './flow resource:publish --collection static'
test-backstop:
stage: test
dependencies:
- build
variables:
GIT_STRATEGY: none
FLOW_CONTEXT: Development/VisualRegressionTesting
artifacts:
name: "BackstopJS Result"
when: always
paths:
- 'backstop.json'
- 'Test/BackstopJS'
- 'Data/Temporary/BackstopJS
expire_in: 1 hrs
script:
- './flow server:run --port 8080 &> /dev/null &'
- './flow backstop:configuration --package-key Vendor.Site --base-uri http://127.0.0.1:8080 > backstop.json'
- 'backstop test --config=backstop.json'
Quellen:
- BackstopJS: http://garris.github.io/BackstopJS/
- Sitegeist.Monocle: https://github.com/sitegeist/Sitegeist.Monocle
- Sitegeist.Monocle.BackstopJS: https://github.com/sitegeist/Sitegeist.Monocle.BackstopJS
- Sitegeist.Kaleidoscope: https://github.com/sitegeist/Sitegeist.Kaleidoscope
- Sitegeist.Lazybones: https://github.com/sitegeist/Sitegeist.Lazybones