Visuelle Regressionstests mit Cypress: Ein pragmatischer Ansatz

Veröffentlicht: 2022-03-11

Jedes Mal, wenn eine neue Version unserer Komponentenbibliothek Picasso veröffentlicht wird, aktualisieren wir alle unsere Front-End-Anwendungen, um das Beste aus den neuen Funktionen herauszuholen und unsere Designs auf alle Teile unserer Website auszurichten.

Letzten Monat haben wir ein Picasso-Update für das Toptal Talent Portal eingeführt, die Plattform, die unsere Talente nutzen, um Jobs zu finden und mit Kunden zu interagieren. Da wir wussten, dass die Veröffentlichung mit großen Designänderungen einhergehen würde, und um unerwartete Probleme zu minimieren, war es sinnvoll, visuelle Regressionstesttechniken einzusetzen, um uns zu helfen, Probleme vor der Veröffentlichung zu finden.

Visuelle Regressionstests sind kein neues Konzept; Viele andere Projekte bei Toptal verwenden es bereits, einschließlich Picasso selbst.

Tools wie Percy, Happo und Chromatic können verwendet werden, um Teams beim Aufbau einer gesunden visuellen Regressionspipeline zu unterstützen, und wir haben zunächst überlegt, sie hinzuzufügen. Letztendlich entschieden wir, dass der Einrichtungsprozess zu zeitaufwändig wäre und unseren Zeitplan durcheinanderbringen könnte. Wir hatten bereits ein Datum für das Einfrieren des Codes festgelegt, um mit der Migration zu beginnen, und da nur noch wenige Tage bis zum Stichtag verblieben, blieb uns keine andere Wahl, als kreativ zu sein.

Visuelle Regressionstests durch UI-Tests

Während wir im Projekt keine visuellen Regressionstests hatten, hatten wir eine gute Abdeckung von UI-Integrationstests mit Cypress. Auch wenn das Tool nicht hauptsächlich dafür verwendet wird, hat Cypress eine Seite in seiner Dokumentation, die dem visuellen Testen gewidmet ist, und eine andere, die alle verfügbaren Plug-Ins auflistet, um Cypress für visuelle Tests zu konfigurieren.

Von Cypress zu Screenshots

Nachdem wir die verfügbare Dokumentation durchgesehen hatten, entschieden wir uns, cypress-snapshot-plugin auszuprobieren. Die Einrichtung dauerte nur wenige Minuten, und als wir das getan hatten, stellten wir schnell fest, dass wir nicht auf der Suche nach einer traditionellen visuellen Regressionsausgabe waren.

Die meisten visuellen Regressionstools helfen dabei, unerwünschte Änderungen zu identifizieren, indem sie Snapshots vergleichen und Pixelunterschiede zwischen einer bekannten, akzeptierten Baseline und der geänderten Version einer Seite oder einer Komponente erkennen. Wenn die Pixeldifferenz größer als eine festgelegte Toleranzschwelle ist, wird die Seite oder Komponente markiert, um manuell untersucht zu werden. Wir wussten jedoch, dass wir in dieser Version mehrere kleine Änderungen an den meisten unserer UI-Komponenten vornehmen würden, sodass das Festlegen eines Schwellenwerts nicht anwendbar war. Selbst wenn eine bestimmte Komponente zu 100 % anders war, kann sie im Kontext der neuen Version immer noch korrekt sein. Ebenso könnte eine Abweichung von nur wenigen Pixeln bedeuten, dass ein Bauteil derzeit nicht für die Produktion geeignet ist.

Screenshot, der das erwartete Ergebnis und das tatsächliche Ergebnis des Testlaufs darstellt.
Abbildung 1. Beispiel für geringfügige Pixelunterschiede, die zu falschen Negativen führen

An diesem Punkt wurden zwei gegensätzliche Dinge klar: Das Feststellen von Pixelunterschieden würde nicht helfen, Probleme zu identifizieren, und ein direkter Vergleich der Komponenten war genau das, was wir brauchten. Wir haben das Snapshot-Plug-in beiseite gelegt und uns daran gemacht, eine Sammlung von Bildern mit unseren Komponenten vor und nach der Anwendung des Picasso-Updates zu erstellen. Auf diese Weise konnten wir alle Änderungen schnell durchgehen, um festzustellen, ob die neuen Versionen immer noch den Anforderungen der Website und den Standards der Bibliothek entsprachen.

Der neue Plan war, einen Screenshot einer Komponente zu machen, ihn lokal zu speichern, einen neuen Screenshot derselben Komponente im Zweig mit der aktualisierten Picasso-Version zu machen und sie dann zu einem einzigen Bild zusammenzuführen. Letztendlich unterschied sich dieser neue Ansatz nicht allzu sehr von dem, mit dem wir begonnen hatten, aber er gab uns mehr Flexibilität während der Implementierungsphase, da wir das Plug-in nicht mehr importieren und seine neuen Befehle verwenden mussten.

Diagramm, das einen visuellen Vergleichsablauf zeigt, wie Bilder der neuen und alten Version nach dem visuellen Testlauf zusammengeführt werden.
Abbildung 2. Fluss des visuellen Vergleichs

Nutzung von APIs für Vergleichsbilder

Mit einem klaren Ziel vor Augen war es an der Zeit, uns anzusehen, wie Cypress uns dabei helfen könnte, die benötigten Screenshots zu erhalten. Wie bereits erwähnt, hatten wir viele UI-Tests, die den Großteil des Talentportals abdeckten. Um so viele kritische Komponenten wie möglich zu sammeln, haben wir uns entschieden, nach jeder Interaktion Screenshots von einzelnen Elementen zu machen.

Ein alternativer Ansatz wäre gewesen, Screenshots der gesamten Seite in Schlüsselmomenten während des Tests zu machen, aber wir entschieden, dass diese Bilder zu schwer zu vergleichen wären. Außerdem könnten solche Vergleiche anfälliger für menschliche Fehler sein, z. B. wenn übersehen wird, dass sich eine Fußzeile geändert hat.

Eine dritte Option wäre gewesen, jeden einzelnen Testfall durchzugehen, um zu entscheiden, was erfasst werden soll, aber das hätte viel mehr Zeit in Anspruch genommen, sodass das Festhalten an allen auf den Seiten verwendeten Elementen ein praktischer Kompromiss zu sein schien.

Wir wandten uns an die API von Cypress, um die Bilder zu generieren. Der Befehl cy.screenshot() kann standardmäßig individuelle Bilder von Komponenten erstellen, und die After Screenshot API ermöglicht es uns, Dateien umzubenennen, Verzeichnisse zu wechseln und visuelle Regressionsläufe von Standardläufen zu unterscheiden. Durch die Kombination der beiden haben wir Läufe erstellt, die unsere Funktionstests nicht beeinträchtigten und es uns ermöglichten, Bilder in den entsprechenden Ordnern zu speichern.

Zuerst haben wir die Datei index.js in unserem Plug-in-Verzeichnis erweitert, um die beiden neuen Lauftypen (Baseline und Vergleich) zu unterstützen. Dann legen wir den Pfad für unsere Bilder entsprechend dem Ausführungstyp fest:

 // plugins/index.js const fs = require('fs') const path = require('path') module.exports = (on, config) => { // Adding these values to your config object allows you to access them in your tests. config.env.baseline = process.env.BASELINE || false config.env.comparison = process.env.COMPARISON || false on('after:screenshot', details => { // We only want to modify the behavior of baseline and comparison runs. if (config.env.baseline || config.env.comparison) { // We keep track of the file name and number to make sure they are saved in the proper order and in their relevant folders. // An alternative would have been to look up the folder for the latest image, but this was the simpler approach. let lastScreenshotFile = '' let lastScreenshotNumber = 0 // We append the proper suffix number to the image, create the folder, and move the file. const createDirAndRename = filePath => { if (lastScreenshotFile === filePath) { lastScreenshotNumber++ } else { lastScreenshotNumber = 0 } lastScreenshotFile = filePath const newPath = filePath.replace( '.png', ` #${lastScreenshotNumber}.png` ) return new Promise((resolve, reject) => { fs.mkdir(path.dirname(newPath), { recursive: true }, mkdirErr => { if (mkdirErr) { return reject(mkdirErr) } fs.rename(details.path, newPath, renameErr => { if (renameErr) { return reject(renameErr) } resolve({ path: newPath }) }) }) }) } const screenshotPath = `visualComparison/${config.env.baseline ? 'baseline' : 'comparison'}` return createDirAndRename(details.path .replace('cypress/integration', screenshotPath) .replace('All Specs', screenshotPath) ) } }) return config }

Dann haben wir jeden der Läufe aufgerufen, indem wir die entsprechende Umgebungsvariable zum Cypress-Aufruf in der package.json des Projekts hinzugefügt haben:

 "scripts": { "cypress:baseline": "BASELINE=true yarn cypress:open", "cypress:comparison": "COMPARISON=true yarn cypress:open" }

Nachdem wir unsere neuen Befehle ausgeführt hatten, konnten wir sehen, dass alle während des Laufs aufgenommenen Screenshots in die entsprechenden Ordner verschoben wurden.

Ein Screenshot mit Bildern, die während des Laufs aufgenommen und in Ordner verschoben wurden.
Abbildung 3. Visuelle Laufergebnisse

Als nächstes haben wir versucht, cy.get() , den Hauptbefehl von Cypress, um DOM-Elemente zurückzugeben, zu überschreiben und einen Screenshot aller aufgerufenen Elemente zusammen mit ihrer Standardimplementierung zu machen. Leider ist cy.get() ein schwierig zu ändernder Befehl, da der Aufruf des ursprünglichen Befehls in seiner eigenen Definition zu einer Endlosschleife führt. Der vorgeschlagene Ansatz zur Umgehung dieser Einschränkung besteht darin, einen separaten benutzerdefinierten Befehl zu erstellen und diesen neuen Befehl dann einen Screenshot machen zu lassen, nachdem das Element gefunden wurde:

 Cypress.Commands.add("getAndScreenshot", (selector, options) => { // Note: You might need to tweak the command when getting multiple elements. return cy.get(selector).screenshot() }); it("get overwrite", () => { cy.visit("https://example.cypress.io/commands/actions"); cy.getAndScreenshot(".action-email") })

Allerdings waren unsere Aufrufe zur Interaktion mit Elementen auf der Seite bereits in eine interne getElement() Funktion eingeschlossen. Wir mussten also nur sicherstellen, dass beim Aufruf des Wrappers ein Screenshot gemacht wurde.

Durch visuelle Regressionstests erhaltene Ergebnisse

Sobald wir die Screenshots hatten, mussten wir sie nur noch zusammenführen. Dafür haben wir mit Canvas ein einfaches Knotenskript erstellt. Am Ende hat uns das Skript ermöglicht, 618 Vergleichsbilder zu generieren! Einige der Unterschiede waren beim Öffnen des Talentportals leicht zu erkennen, aber einige der Probleme waren nicht so offensichtlich.

Vorher-Nachher-Beispiel für die falsche Verwendung von Picasso, bei dem rote und schwarze Farben im Element angezeigt werden.
Abbildung 4. Beispiel für das Nichtbefolgen der neuen Picasso-Richtlinien; ein Unterschied wurde erwartet, aber die neue Version hätte einen roten Hintergrund und weißen Text haben sollen

Vorher-Nachher-Beispiel eines leicht fehlerhaften Komponentenlayouts, das falsch ausgerichteten Text neben einem Kontrollkästchen im „Nachher“-Bild zeigt.
Abbildung 5. Beispiel für ein leicht gebrochenes Komponentenlayout

Mehrwert für UI-Tests

Zunächst einmal erwiesen sich die hinzugefügten visuellen Regressionstests als nützlich und deckten einige Probleme auf, die wir ohne sie übersehen hätten. Obwohl wir mit Unterschieden in unseren Komponenten gerechnet hatten, half das Wissen, was tatsächlich geändert wurde, dabei, problematische Fälle einzugrenzen. Also, wenn Ihr Projekt eine Schnittstelle hat, Sie diese Tests aber noch nicht durchführen, machen Sie es!

Die zweite Lektion hier, und vielleicht die wichtigere, ist, dass wir wieder einmal daran erinnert wurden, dass das Perfekte der Feind des Guten ist. Wenn wir die Möglichkeit ausgeschlossen hätten, visuelle Regressionstests für diese Version durchzuführen, weil es keine vorherige Einrichtung gab, sind uns während der Migration möglicherweise einige Fehler entgangen. Stattdessen einigten wir uns auf einen Plan, der zwar nicht ideal, aber schnell umzusetzen war, wir arbeiteten darauf hin und er zahlte sich aus.

Weitere Einzelheiten zur Implementierung einer robusten visuellen Regressionspipeline in Ihrem Projekt finden Sie auf der Seite für visuelle Tests von Cypress, wählen Sie das Tool aus, das Ihren Anforderungen am besten entspricht, und sehen Sie sich die Lernvideos an.