Articles

Test-Workflow für Webkomponenten

Wenn Sie etwas ausliefern, das von anderen genutzt werden soll, übernehmen Sie die Verantwortung, sicheren und stabilen Code zu liefern. Eine Möglichkeit, dem zu begegnen, ist das Testen Ihres Codes.

Ganz gleich, wie klein – ganz gleich, wie einfach Ihr Projekt ist, es sollte idealerweise immer Tests geben.

Ja, ich weiß, dass die Realität hart zuschlägt und es viele Fälle geben wird, in denen das nicht passiert – aber Sie sollten immer danach streben, Tests zu haben

Haftungsausschluss

In diesem Tutorial werden wir eine einfache Version eines Eingabeelements erstellen. Am Ende werden Sie die Fähigkeiten und das Wissen erlangen, um die Open-WC-Testing-Tools in die Praxis umzusetzen und eine solide, zugängliche und gut getestete Eingabekomponente zu erstellen.

Warnung

Dies ist ein ausführliches Tutorial, das einige Fallstricke und schwierige Fälle bei der Arbeit mit Webkomponenten zeigt. Dies ist definitiv für fortgeschrittene Benutzer. Sie sollten ein Grundwissen über LitElement und JSDoc Types haben. Eine Vorstellung davon zu haben, was Mocha, Chai BDD, Karma ist, könnte auch ein wenig helfen.

Wir denken darüber nach, eine leichter verdauliche Version davon zu veröffentlichen, also wenn das etwas ist, was Sie gerne sehen würden – lassen Sie es uns in den Kommentaren wissen.

Wenn Sie mitspielen wollen – der ganze Code ist auf github.

Legen wir los!

Ausführen in deiner Konsole

$ npm init @open-wc# Results in this flow✔ What would you like to do today? › Scaffold a new project✔ What would you like to scaffold? › Web Component# Select with space! "Testing" => just enter will move one with no selection✔ What would you like to add? › Testing✔ Would you like to scaffold examples files for? › Testing✔ What is the tag name of your application/web component? … a11y-input✔ Do you want to write this file structure to disk? › YesWriting..... done✔ Do you want to install dependencies? › No

Für weitere Details siehe https://open-wc.org/testing/.

Löschen Sie src/A11yInput.js

Ändern Sie src/a11y-input.js zu:

und test/a11y-input.test.js zu:

Unsere Tests bestehen bisher aus einem einzigen Merkmal (der Eigenschaft label) und einer einzigen Behauptung (expect). Wir verwenden die BDD-Syntax von Karma und Chai, also gruppieren wir Testgruppen (it) unter den Features oder APIs, auf die sie sich beziehen (describe).

Lassen Sie uns sehen, ob alles korrekt funktioniert, indem wir es ausführen: npm run test.

Awesome – genau wie erwartet (🥁), haben wir einen fehlgeschlagenen Test 🙂

Lassen Sie uns in den Überwachungsmodus wechseln, der die Tests kontinuierlich laufen lässt, sobald Sie Änderungen an Ihrem Code vornehmen.

npm run test:watch

Der folgende Code wurde im Video oben zu src/a11y-input.js hinzugefügt:

static get properties() { return { label: { type: String }, };}constructor() { super(); this.label = '';}

So weit, so gut? Immer noch dabei? Prima! Lassen Sie uns das Spiel ein wenig aufpeppen…

Hinzufügen eines Tests für das Schatten-DOM

Lassen Sie uns eine Behauptung hinzufügen, um den Inhalt der Schattenwurzel unseres Elements zu testen.

Wenn wir sicher sein wollen, dass sich unser Element gleich verhält/ausschaut, sollten wir sicherstellen, dass seine Dom-Struktur gleich bleibt.
Lassen Sie uns also das aktuelle Schatten-DOM mit dem vergleichen, was wir wollen.

Wie erwartet, erhalten wir:

So lasst uns das in unserem Element implementieren.

render() { return html` <slot name="label"></slot> <slot name="input"></slot> `;}

Interessant, der Test sollte grün sein… ist er aber nicht 🤔 Schauen wir uns das mal an.

Du hast vielleicht diese seltsamen leeren Kommentar <!----> Tags bemerkt. Das sind Markierungen, die lit-html verwendet, um sich zu merken, wo dynamische Teile sind, damit sie effizient aktualisiert werden können. Beim Testen kann dies jedoch ein wenig lästig sein.

Wenn wir innerHTML verwenden, um das DOM zu vergleichen, müssen wir uns auf einfache String-Gleichheit verlassen. Unter diesen Umständen müssten wir den Leerraum, die Kommentare usw. des generierten DOMs genau abgleichen; mit anderen Worten: es muss eine perfekte Übereinstimmung sein. Alles, was wir testen müssen, ist, dass die Elemente, die wir rendern wollen, auch gerendert werden. Wir wollen den semantischen Inhalt der Schattenwurzel testen.

So lasst es uns ausprobieren 💪

// old:expect(el.shadowRoot.innerHTML).to.equal(`...`);// new:expect(el).shadowDom.to.equal(` <slot name="label"></slot> <slot name="input"></slot>`);

Bam 🎉

a11y input ✔ has by default an empty string as a label ✔ has a static shadowDom

Wie funktioniert shadowDom.to.equal()?

  1. Es holt das innerHTML der Schattenwurzel
  2. analysiert es (eigentlich, der Browser parst es – keine Bibliothek wird benötigt)
  3. Normalisiert es (potentiell jedes Tag/Eigenschaft in einer eigenen Zeile)
  4. Parst und normalisiert den erwarteten HTML-String
  5. Übergibt beide normalisierten DOM-Strings an die Standard-Vergleichsfunktion von chai
  6. Im Falle eines Fehlers, gruppiert und zeigt alle Unterschiede übersichtlich an

Wenn Sie mehr wissen wollen, schauen Sie bitte in die Dokumentation von semantic-dom-diff.

Testen des „Light“-DOM

Wir können genau das Gleiche mit dem Light-DOM machen. (Das DOM, das von unserem Benutzer oder unseren Vorgaben bereitgestellt wird, d.h. das children des Elements).

Und das wollen wir implementieren.

So haben wir unser Licht- und Schatten-DOM getestet 💪 und unsere Tests laufen sauber 🎉

Hinweis: Die Verwendung der DOM-API im Lebenszyklus eines Lichtelements ist ein Anti-Pattern, aber um a11y zu ermöglichen, könnte es ein echter Anwendungsfall sein – auf jeden Fall ist es großartig für Illustrationszwecke

Unser Element in einer App verwenden

So, jetzt, wo wir eine grundlegende a11y-Eingabe haben, lasst es uns verwenden – und testen – in unserer Anwendung.

Wieder beginnen wir mit einem Skelett src/my-app.js

Und unser Test in test/my-app.test.js;

Ausführen des Tests => schlägt fehl und dann fügen wir die Implementierung in src/a11y-input.js

render() { return html` <h1>My Filter App</h1> <a11y-input></a11y-input> `;}

Aber oh nein! Das sollte jetzt grün sein…

Was ist los?
Erinnern Sie sich, dass wir einen speziellen Test hatten, um das Licht-Dom von a11y-input sicherzustellen?
So, selbst wenn der Benutzer nur <a11y-input></a11y-input> in seinen Code einfügt – was tatsächlich herauskommt, ist

<a11y-input> <label slot="label"></label> <input slot="input"></a11y-input>

z.B. a11y-inputerzeugt tatsächlich Knoten innerhalb Ihres my-appSchatten-Doms. Absurd! Für unser Beispiel hier sagen wir, dass wir das wollen.
Wie können wir es also trotzdem testen?

Glücklicherweise hat .shadowDom noch ein weiteres Ass im Ärmel; es erlaubt uns, Teile von dom zu ignorieren.

expect(el).shadowDom.to.equal(` <h1>My Filter App</h1> <a11y-input></a11y-input>`, { ignoreChildren: });

Wir können sogar die folgenden Eigenschaften mit angeben:

  • ignoreChildren
  • ignoreTags
  • ignoreAttributes (global oder für bestimmte Tags)

Für weitere Details siehe semantic-dom-diff.

Testen von Snapshots

Wenn man viele große Dom-Bäume hat, wird es schwierig, all diese manuell geschriebenen Erwartungen zu schreiben und zu pflegen.
Um dabei zu helfen, gibt es halb-automatische Snapshots.

Wenn wir also unseren Code ändern

// fromexpect(el).shadowDom.to.equal(` <slot name="label"></slot> <slot name="input"></slot>`);// toexpect(el).shadowDom.to.equalSnapshot();

Wenn wir nun npm run test ausführen, wird eine Datei __snapshots__/a11y input.md erstellt und mit etwas wie diesem gefüllt

# `a11y input`#### `has a static shadowDom```html<slot name="label"></slot><slot name="input"></slot>``

Was wir vorher von Hand geschrieben haben, kann nun automatisch bei init oder zwangsweise über npm run test:update-snapshots generiert werden.

Wenn die Datei __snapshots__/a11y input.md bereits existiert, wird sie mit der Ausgabe verglichen und man bekommt Fehler, wenn die HTML-Ausgabe geändert wurde.

Für weitere Details siehe semantic-dom-diff.

Ich denke, das ist jetzt genug über den Vergleich von Dom-Bäumen…
Es ist Zeit für eine Veränderung 🤗

Codeabdeckung

Eine weitere nützliche Metrik, die wir beim Testen mit dem open-wc Setup erhalten, ist die Codeabdeckung.
Was bedeutet sie also und wie können wir sie erhalten? Die Codeabdeckung ist ein Maß dafür, wie viel von unserem Code durch Tests überprüft wird. Wenn es eine Zeile, eine Anweisung, eine Funktion oder eine Verzweigung (z.B. if/else) gibt, die von unseren Tests nicht abgedeckt wird, wirkt sich das auf die Abdeckungsrate aus.
Ein einfaches npm run test genügt, und Sie erhalten folgendes Ergebnis:

=============================== Coverage summary ===============================Statements : 100% ( 15/15 )Branches : 100% ( 0/0 )Functions : 100% ( 5/5 )Lines : 100% ( 15/15 )================================================================================

Das bedeutet, dass 100% der Anweisungen, Verzweigungen, Funktionen und Zeilen unseres Codes von Tests abgedeckt werden. Sehr schön!

Lassen Sie uns also den umgekehrten Weg gehen und Code zu src/a11y-input.js hinzufügen, bevor wir einen Test hinzufügen. Nehmen wir an, wir wollen über unser benutzerdefiniertes Element direkt auf den Wert unserer Eingabe zugreifen und immer dann, wenn der Wert „cat“ ist, wollen wir etwas protokollieren.

Das Ergebnis sieht ganz anders aus

Unser Abdeckungsgrad ist viel geringer als vorher. Unser Testbefehl schlägt sogar fehl, obwohl alle Tests erfolgreich laufen.
Das liegt daran, dass die Konfiguration von open-wc standardmäßig einen Schwellenwert von 90% für die Codeabdeckung festlegt.

Wenn wir die Abdeckung verbessern wollen, müssen wir Tests hinzufügen – also machen wir es

uh oh 😱 wir wollten die Abdeckung verbessern, aber jetzt müssen wir zuerst einen tatsächlichen Fehler beheben 😞

Das war unerwartet… auf den ersten Blick weiß ich nicht wirklich, was das bedeutet… besser ist es, einige tatsächliche Knoten zu prüfen und sie im Browser zu untersuchen.

Debugging im Browser

Wenn wir unseren Test mit watch ausführen, richtet Karma eine persistente Browserumgebung ein, in der die Tests ausgeführt werden.

  • Versichern Sie sich, dass Sie mit npm run test:watch
  • Besuch http://localhost:9876/debug.html

Sie sollten so etwas sehen

Sie können auf den eingekreisten Play-Button klicken, um nur einen einzelnen Test auszuführen.

Öffnen wir also die Chrome Dev Tools (F12) und fügen einen Debugger in den Testcode ein.

Dang… der Fehler tritt schon vor diesem Punkt auf…
„Fatale“ Fehler wie dieser sind etwas schwieriger, da es sich nicht um fehlgeschlagene Tests handelt, sondern um eine Art kompletten Zusammenbruch der gesamten Komponente.

Ok, fügen wir etwas Code direkt in den setter ein.

set value(newValue) { debugger;

Alles klar, das hat funktioniert, also schreiben wir in unsere Chrome-Konsole console.log(this)Lassen Sie uns sehen, was wir hier haben

<a11y-input> #shadow-root (open)</a11y-input>

Ahh, da haben wir es – der Schatten-Dom ist noch nicht gerendert, wenn der Setter aufgerufen wird.
So gehen wir auf Nummer sicher und fügen eine Prüfung vorher hinzu

set value(newValue) { if (newValue === 'cat') { console.log('We like cats too :)'); } if (this.inputEl) { this.inputEl.value = newValue; }}

Fatel-Fehler ist weg 🎉
Aber wir haben jetzt einen fehlgeschlagenen Test 😭

✖ can set/get the input value directly via the custom elementAssertionError: expected '' to equal 'foo'

Wir brauchen vielleicht eine andere Taktik 🤔
Wir können es als separate value Eigenschaft hinzufügen und bei Bedarf synchronisieren.

Und wir sind endlich wieder im Geschäft! 🎉

ok Fehler behoben – können wir bitte zur Abdeckung zurückkehren? danke 🙏

Zurück zur Abdeckung

Mit diesem hinzugefügten Test haben wir einige Fortschritte gemacht.

Allerdings sind wir immer noch nicht ganz da – die Frage ist warum?

Um das herauszufinden, öffne coverage/index.html in deinem Browser. Es wird kein Webserver benötigt, öffnen Sie einfach die Datei in Ihrem Browser – auf einem Mac können Sie das von der Kommandozeile aus mit open coverage/index.html

Sie werden etwas wie dieses sehen

Wenn Sie auf a11y-input.js klicken, erhalten Sie eine zeilenweise Information, wie oft sie ausgeführt wurden.
So können wir sofort sehen, welche Zeilen von unseren Tests noch nicht ausgeführt wurden.

So fügen wir einen Test dafür hinzu

=============================== Coverage summary ===============================Statements : 100% ( 24/24 )Branches : 75% ( 3/4 )Functions : 100% ( 7/7 )Lines : 100% ( 24/24 )================================================================================

Damit sind wir wieder bei 100% bei den Anweisungen, aber es fehlt immer noch etwas bei den Verzweigungen.
Lassen Sie uns sehen warum?

Das E bedeutet else path not taken.
Wenn also die Funktion update aufgerufen wird, gibt es immer eine Eigenschaft value in den changedProperties.

Wir haben auch label, also ist es eine gute Idee, das zu testen. 👍

Boom 100% 💪 wir gewinnen 🥇

=============================== Coverage summary ===============================Statements : 100% ( 24/24 )Branches : 100% ( 4/4 )Functions : 100% ( 7/7 )Lines : 100% ( 24/24 )================================================================================

Aber warte, wir haben nicht einmal den Test oben beendet – der Code ist immer noch

 // somehow check that console.log was called

Wie kommt es, dass wir 100% Testabdeckung haben?

Lassen Sie uns zunächst versuchen zu verstehen, wie die Codeabdeckung funktioniert 🤔
Die Art und Weise, wie die Codeabdeckung gemessen wird, ist durch die Anwendung einer Form von instrumentation. Kurz gesagt, bevor unser Code ausgeführt wird, wird er verändert (instrumented) und verhält sich in etwa wie folgt:

Anmerkung: Dies ist eine stark vereinfachte Version zur Veranschaulichung.

Grundsätzlich wird dein Code mit vielen, vielen Flags übersät. Je nachdem, welche Flags ausgelöst werden, wird eine Statistik erstellt.

100% Testabdeckung bedeutet also nur, dass jede Zeile deines Codes mindestens einmal ausgeführt wurde, nachdem alle deine Tests abgeschlossen waren. Es bedeutet nicht, dass Sie alles getestet haben, oder ob Ihre Tests die richtigen Aussagen machen.

Auch wenn wir bereits eine 100%ige Codeabdeckung haben, werden wir unseren Protokolltest noch verbessern.

Sehen Sie daher die Codeabdeckung als ein Werkzeug, das Ihnen nur eine Hilfestellung beim Aufspüren fehlender Tests gibt, und nicht als eine harte Garantie für die Codequalität.

Code ausspionieren

Wenn Sie überprüfen wollen, wie oft oder mit welchen Parametern eine Funktion aufgerufen wird, nennt man das Spionieren.
open-wc empfiehlt das ehrwürdige sinon-Paket, das viele Werkzeuge für das Spionieren und andere verwandte Aufgaben bereitstellt.

npm i -D sinon

So erstellt man einen Spion für ein bestimmtes Objekt und kann dann prüfen, wie oft es aufgerufen wird.

Ach ja… der Test schlägt fehl:

AssertionError: expected 0 to equal 1

Das Hantieren mit globalen Objekten wie console könnte Nebenwirkungen haben, also sollten wir besser eine dedizierte Log-Funktion verwenden.

Das führt dazu, dass wir kein globales Objekt in unserem Testcode haben – toll 🤗

Allerdings bekommen wir immer noch den gleichen Fehler. Let’s debug… boohoo anscheinend ist update nicht sync – eine falsche Annahme, die ich gemacht habe 🙈 Ich sage, dass Annahmen oft gefährlich sind – trotzdem falle ich von Zeit zu Zeit darauf rein 😢.

So was können wir tun? Leider scheint es keine öffentliche API zu geben, um einige Sync-Aktionen auszuführen, die durch eine Eigenschaftsaktualisierung ausgelöst werden.
Lassen Sie uns ein Issue dafür erstellen https://github.com/Polymer/lit-element/issues/643.

Für den Moment scheint der einzige Weg zu sein, sich auf eine private API zu verlassen. 🙈
Außerdem mussten wir die Wertesynchronisation nach updated verschieben, damit sie nach jedem Dom-Rendering ausgeführt wird.

Und hier ist der aktualisierte Test für das Logging

Wow, das war ein bisschen schwieriger als erwartet, aber wir haben es geschafft 💪

SUMMARY:✔ 7 tests completedTOTAL: 7 SUCCESS

Tests ohne Karma Framework durchführen

Das Karma Framework ist mächtig und funktionsreich, aber manchmal wollen wir unser Testregiment vielleicht etwas reduzieren. Das Schöne an allem, was wir bisher vorgeschlagen haben, ist, dass wir nur Browser-Standard-Module verwendet haben, ohne dass eine Transpilierung nötig war, mit der einzigen Ausnahme von bloßen Modul-Spezifizierern.
So genügt es, ein test/index.html zu erstellen.

und es über owc-dev-server in Chrome öffnen, wird es perfekt funktionieren.
Wir haben alles ohne webpack oder karma zum Laufen gebracht – toll 🤗

Do the Cross-Browser Thing

Wir fühlen uns jetzt ziemlich wohl mit unserer Webkomponente. Sie ist getestet und abgedeckt; es gibt nur noch einen weiteren Schritt – wir wollen sicherstellen, dass sie in allen Browsern läuft und getestet ist.

So let’s just run it

npm run test:bsSUMMARY:✔ 42 tests completedTOTAL: 42 SUCCESS

Ja, das funktioniert gut! 🤗

Wenn es fehlgeschlagene Tests gibt, werden sie in der Zusammenfassung mit dem spezifischen Browser ausgegeben, bei dem sie fehlgeschlagen sind.

Wenn du einen bestimmten Browser debuggen musst:

Auch wenn du den Browser, der getestet wird, anpassen willst, kannst du deinen karma.bs.config.js anpassen.

Zum Beispiel, wenn du den Firefox ESR zu deiner Liste hinzufügen möchtest.

Oder vielleicht möchtest du nur 2 bestimmte Browser testen?

Hinweis: Dies verwendet die Webpack Merge-Strategien replace.

Quick Recap

  • Testen ist wichtig für jedes Projekt. Stellen Sie sicher, dass Sie so viele wie möglich schreiben.
  • Versuchen Sie, Ihre Codeabdeckung hoch zu halten, aber denken Sie daran, dass es keine magische Garantie ist, also muss es nicht immer 100% sein.
  • Debuggen Sie im Browser über npm run test:watch. Für Legacy-Browser verwenden Sie npm run test:legacy.watch.

Wie geht es weiter?

  • Lassen Sie die Tests in Ihrem CI laufen (funktioniert perfekt mit browserstack). Siehe unsere Empfehlungen unter Automatisieren.

Folgen Sie uns auf Twitter, oder folgen Sie mir auf meinem persönlichen Twitter.
Sieh dir auch unsere anderen Tools und Empfehlungen auf open-wc.org an.

Danke an Pascal und Benny für ihr Feedback und ihre Hilfe dabei, aus meinen Scribbles eine nachvollziehbare Geschichte zu machen.