Storybook ist ein komponentengetriebenes Werkzeug für die Erstellung von visuellen Styleguides und zur Demonstration von UI-Komponenten aus React, Angular, Vue sowie Web Components.
Speziell das Snapshot Testing bietet die Möglichkeit, ungewollte Anpassungen des Stylings frühzeitig zu erkennen und zu korrigieren.
Snapshot Testing in Storybook
Snapshot-Tests sind ein sehr nützliches Werkzeug, wenn Sie sicherstellen möchten, dass sich Ihre Benutzeroberfläche nicht unerwartet ändert.
Ein typischer Snapshot-Testfall rendert eine UI-Komponente, erstellt einen Snapshot und vergleicht ihn dann mit einer Referenz-Snapshot-Datei, die neben dem Test gespeichert ist. Der Test schlägt fehl, wenn die beiden Snapshots nicht übereinstimmen: Entweder ist die Änderung unerwartet oder der Referenz-Snapshot muss auf die neue Version der UI-Komponente aktualisiert werden.
Storybook bietet mehrere Möglichkeiten, eine Anwendung zu testen. Angefangen mit Chromatic. Diese sogenannte Werkzeugkette setzt allerdings voraus, dass der Quellcode in GitHub versioniert ist und kostet für den professionellen Gebrauch eine monatliche Gebühr.
Eine weitere Möglichkeit ist das schlanke Add-on Storyshots, welches auf dem Testframework “Jest” basiert. Es wird in der Kommandozeile gestartet und listet dort Abweichungen der Komponente zum vorherigen Stand auf. Der Programmierer muss prüfen, ob diese Änderung erwünscht oder ein Fehler ist.
Einrichtung für Angular
Diese Anleitung setzt voraus, dass Storybook bereits für die Angular-Anwendung installiert ist. Eine Setup-Anleitung finden Sie unter diesem Link. Angular bringt von Haus aus die Testumgebung Karma mit. Um die Anwendung auf Jest umzustellen, sind folgende Schritte nötig:
Installation der Jest Dependencies
Zum Installieren von Jest einfach die Zeile „npm install jest jest-preset-angular –save-dev“ in der Kommandozeile ausführen.
Jest Setup-Datei erstellen
Im root-Verzeichnis ihres Angular Projektes die neue Typescript-Datei setupJest.ts mit dem Inhalt import ‚jest-preset-angular‘; erzeugen.
package.json anpassen
Die package.json ihres Angular-Projektes muss um einen Absatz für das Testframework Jest ergänzt werden:
{ "jest": { "preset": "jest-preset-angular", "setupFilesAfterEnv": [ "<rootDir>/setupJest.ts" }, }
Außerdem muss die Script-Ausführung für Test angepasst werden. Anstatt “test“: “ngtest“, muss “test“: “jest” verwendet werden.
Karma entfernen (optional)
Zum Entfernen von Karma muss folgende Kommandozeile ausgeführt werden:
npm uninstall karma karma-chrome-launcher karma-coverage-istanbul-reporter karma-jasmine karma-jasmine-html-reporter
Im Anschluss sollten auch die Dateien Karma.config.js und test.ts im <root>/src Verzeichnis gelöscht werden und der Abschnitt für Test in der angular.json entfernt werden.
Migration von Jasmine (optional)
Für die Migration nach Jest müssen Anpassungen vorgenommen werden:
• Kommandozeile: npm uninstall @types/jasmine
• jasmine.createSpyObj(’name‘, [‚key‘]) wird zu jest.fn({key: jest.fn()})
• jasmine.createSpy(’name‘) wird zu jest.fn()
• spyOn mit returnValue() muss in jest.spyOn(…).mockReturnValue(…) umgewandelt werden
• spyOn mit callFacke() muss in jest.spyOn(…).mockImplementation(…) umgewandelt werden
• Asymmetric matchers: jasmine.any, jasmine.objectContaining, etc. wird zu expect.any,
expect.objectContaining
Storyshots Dependencies installieren
Nun wird Storyshots installiert. Um Storyshots zu installieren, sollen diese zwei Kommandozeilen ausgeführt werden:
npm i -D @storybook/addon-storyshots npm i -D @storybook/addon-storyshots-puppeteer puppeteer
Nach der Installation sollten folgende Dependencies in der package.json vorhanden sein (Stand 12.11.2021; wichtig für den Installations-Workaround unter Angular):
"jest": "^27.3.1", "jest-preset-angular": "^10.0.1", "@storybook/addon-storyshots": "^6.3.12" "@storybook/addon-storyshots-puppeteer": "^6.3.12"
Storyshots Installationsdatei erstellen
Nach der Installation von Storyshots muss die Erweiterung noch eingestellt werden. Dafür muss im Verzeichnis <root>/src die Datei Storyshorts.test.js mit folgendem Inhalt erstellt werden:
import initStoryshots from '@storybook/addon-storyshots'; import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'; import path from 'path'; // Function to customize the snapshot location const getMatchOptions = ({ context: { fileName } }) => { // Generates a custom path based on the file name and the custom directory. const snapshotPath = path.join(path.dirname(fileName), 'snapshot-images'); return { customSnapshotsDir: snapshotPath }; }; initStoryshots({ // your own configuration test: imageSnapshot({ // invoke the function above here getMatchOptions, }), });
tsconfig.json für Storyshots erweitern
Des Weiteren muss noch die tsconfig.json angepasst werden. Dafür muss der compilerOptions Abschnitt in tsconfig.json wie folgt erweitert werden:
"compilerOptions": { "esModuleInterop": true,
Package.json für Storyshots erweitern
Zuletzt muss der in der Package.json enthaltene Abschnitt für Jest umkonfiguriert werden:
"jest": { "preset": "jest-preset-angular", "setupFilesAfterEnv": [ "<rootDir>/setupJest.ts" ], "transformIgnorePatterns": [ "<rootDir>/node_modules/(?!(@storybook/addon-docs))" ], "moduleNameMapper": { "jest-preset-angular/build/setup-jest": "jest-preset-angular/setup-jest", "jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer": "jest-presetangular/build/serializers/no-ng-attributes", "jest-preset-angular/build/AngularSnapshotSerializer": "jest-presetangular/build/serializers/ng-snapshot", "jest-preset-angular/build/HTMLCommentSerializer": "jest-presetangular/build/serializers/html-comment" } },
Diese Anpassungen sind speziell für die gewählte Version, da die Ordnerstruktur in Jest umgemappt werden muss. Das kann sich in späteren Versionen von Storyshorts wieder ändern.
Test der Komponenten
Für den Test gibt es eine Beispielanwendung, welche aus vier Einzelkomponenten besteht. Die erste Komponente zeigt die Uhrzeit inklusive Datum und Wochentag an. Die zweite Komponente gibt das aktuelle Wetter in Schaubild mit Gradzahl, Tageshöchst- und Tagestiefsttemperatur an. Über eine weitere Komponente werden die Straßenbahnabfahrten am Beispiel Dresden Striesen abgebildet. Zuletzt gibt es noch eine Komponente, welche 3 Aktienkurse anzeigt mitsamt Graph und Indikatoren.
Bespielhaft sieht der Quellcode der Uhrzeitkomponente wie folgt aus:
export default { title: 'WidgetMonitor/Clock', component: ClockComponent, timeIsRunning: false, } as Meta; export const Morning = () => { return({ props: { timeIsRunning: false, time: new Date(2021, 10, 9, 9, 9, 9, 9) }, parameter: { time: new Date(2021, 10, 9, 9, 9, 9, 9) } }) } export const Afternoon = () => { return({ props: { timeIsRunning: false, time: new Date(2021, 10, 9, 15, 15, 15, 15) } }) } const Template: Story<ClockComponent> = (args: ClockComponent) => ({ props: args }); export const Running = Template.bind({}); Running.args = { timeIsRunning: true, }; Running.parameters = { storyshots: { disable: true } };
Diese beinhaltet drei Zustände, von denen die ersten beiden jeweils statische Zeitpunkte sind. Der dritte Zustand “Running” zeigt die aktuelle Uhrzeit an, d. h. er ist nicht statisch.
Voraussetzung für Snapshot Tests unter Storybook
Es ist wichtig, dass wir unter Storybook einen statischen Zustand haben, damit die Anwendung getestet werden kann. Im Uhrzeitbespiel ist der Zustand „Running“ nicht statisch. Diesen kann man überspringen, indem man den parameter storyshots: { disable: true } hinzufügt (siehe Quellcode weiter oben).
Test starten
Mit der Kommandozeile npm test, wird der Test in der Kommandozeile im Angular-Projektverzeichnis gestartet. Der initiale Snapshot Test erstellt nun ein Snapshot Image von jedem Komponentenzustand.
Zum Aufzeigen von Fehlern wird nun beispielhaft die Schrift der Uhrzeit in der Clock-Komponente sowohl kleiner als auch in Rot im SCSS umgestellt und der Test erneut gestartet.
Das Ergebnis des Snapshot Tests zeigt, dass die beiden aktiven Zustände der Clock-Komponente umgefallen sind und auf ein Diff Image verwiesen wird. Dieses sieht wie folgt aus:
Links ist der ursprüngliche Zustand zu sehen, rechts der Zustand nach der Änderung. In der Mitte sieht man den Zustand, wie sich beide überschneiden. Nun gibt es die Möglichkeit, diesen Zustand entweder zu übernehmen oder den Test – nach Anpassung der Anwendung – erneut auszuführen.
Das Übernehmen des Zustands wird mittels der Kommandozeile npm test — -u erzwungen. Damit werden die Differenzbilder gelöscht und ein neuer Snapshot des Zustands der Komponente erstellt. Das erneute Aufrufen der Kommandozeile npm test sollte nun ohne Fehler durchlaufen.
Fazit
Einen Zustand für Storybook zu pflegen, bedeutet auch immer einen Mehraufwand im Projekt. Wer sich vor diesem Aufwand nicht scheut, hat mittels Jest und der Erweiterung StoryShots die Möglichkeit, einen bestimmten Zustand gekapselt prüfen zu können. Dies ist besonders hilfreich zur Früherkennung von Styling Bugs, welche schwierig in Unit- und Ende-Zu-Ende-Tests gefunden werden können und meist erst beim manuellen Testen auffallen.