Articles

Workflow voor het testen van webcomponenten

Wanneer u iets verzendt dat door anderen zal worden gebruikt, neemt u de verantwoordelijkheid op u om veilige en stabiele code af te leveren. Een van de manieren om dit te doen is door je code te testen.

Hoe klein of eenvoudig je project ook is, idealiter zijn er altijd tests.

Ja, ik weet dat de realiteit hard toeslaat en dat er veel gevallen zullen zijn waarin dat niet gebeurt – maar je moet er altijd naar streven om tests te hebben

Disclaimer

In deze tutorial gaan we een eenvoudige versie van een invoerelement maken. Aan het eind van de tutorial zul je de vaardigheden en kennis hebben om open-wc-testtools in de praktijk te brengen en een solide, toegankelijk en goed getest invoercomponent te bouwen.

Waarschuwing

Dit is een diepgaande tutorial die een aantal valkuilen en moeilijke gevallen laat zien bij het werken met webcomponenten. Dit is zeker voor meer gevorderde gebruikers. Je moet een basiskennis hebben van LitElement en JSDoc Types. Een idee hebben wat Mocha, Chai BDD, Karma is, kan ook een beetje helpen.

We denken erover om een licht verteerbare versie hiervan te posten, dus als dat iets is wat je graag zou willen zien – laat het ons weten in de comments.

Als je mee wilt spelen – alle code staat op github.

Laten we aan de slag gaan!

Uitvoeren in uw console

$ 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

Voor meer details, zie https://open-wc.org/testing/.

Verwijder src/A11yInput.js

Verander src/a11y-input.js in:

en test/a11y-input.test.js in:

Onze tests bestaan tot nu toe uit een enkele eigenschap (de label eigenschap) en een enkele bewering (expect). We gebruiken de BDD syntaxis van karma en chai, dus we groeperen sets van tests (it) onder de functies of API’s waarop ze betrekking hebben (describe).

Laten we eens kijken of alles correct werkt door te draaien: npm run test.

Geweldig – precies zoals verwacht (🥁), we hebben een falende test 🙂

Laten we overschakelen naar de waakmodus, die de tests continu zal uitvoeren wanneer u wijzigingen aanbrengt in uw code.

npm run test:watch

De volgende code is in de bovenstaande video toegevoegd aan src/a11y-input.js:

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

Zo ver, zo goed? Nog steeds bij ons? Geweldig! Laten we het spel een beetje opvoeren…

Een test voor schaduw-DOM toevoegen

Laten we een assertie toevoegen om de inhoud van de schaduw-root van ons element te testen.

Als we er zeker van willen zijn dat ons element zich hetzelfde gedraagt/er hetzelfde uitziet, moeten we ervoor zorgen dat de dom-structuur hetzelfde blijft.
Dus laten we de huidige schaduw-dom vergelijken met wat we willen dat het is.

Zoals verwacht, krijgen we:

Dus laten we dat in ons element implementeren.

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

Interessant, de test zou groen moeten zijn… maar dat is niet zo 🤔 Laten we eens een kijkje nemen.

Je hebt misschien die rare lege commentaar <!----> tags opgemerkt. Het zijn markeringen die lit-html gebruikt om te onthouden waar dynamische delen zich bevinden, zodat het efficiënt kan worden bijgewerkt. Voor testen kan dit echter een beetje vervelend zijn om mee om te gaan.

Als we innerHTML gebruiken om het DOM te vergelijken, zouden we moeten vertrouwen op simpele string gelijkheid. Onder die omstandigheden moeten we precies overeenkomen met de witruimte, commentaar, enz. van de gegenereerde DOM; met andere woorden: het moet een perfecte overeenkomst zijn. Eigenlijk hoeven we alleen maar te testen of de elementen die we willen renderen ook worden gerenderd. We willen de semantische inhoud van de shadow root testen.

Dus laten we het eens uitproberen 💪

// 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

Hoe werkt shadowDom.to.equal()?

  1. Het krijgt de innerHTML van de schaduw root
  2. Pars het (eigenlijk, de browser parseert het – geen bibliotheek nodig)
  3. Normaliseert het (potentieel elke tag/property op zijn eigen regel)
  4. Parses en normaliseert de verwachte HTML string
  5. Geeft beide genormaliseerde DOM strings door aan chai’s standaard vergelijkingsfunctie
  6. In geval van mislukking, groepeert, en geeft eventuele verschillen op een duidelijke manier weer

Als u meer wilt weten, kijk dan in de documentatie van semantic-dom-diff.

Het testen van de “Light” DOM

We kunnen precies hetzelfde doen met de light DOM. (De DOM die zal worden geleverd door onze gebruiker of onze standaardinstellingen, d.w.z. de children van het element).

En laten we deze implementeren.

Dus we hebben onze licht- en schaduw-dom getest 💪 en onze tests lopen goed 🎉

Note: Het gebruik van de DOM API in de levenscyclus van een licht-element is een antipatroon, maar om a11y mogelijk te maken kan het een echte use case zijn – in ieder geval is het geweldig ter illustratie

Ons element in een app gebruiken

Dus nu we een basis a11y input hebben, laten we het gebruiken – en het testen – in onze applicatie.

Wederom beginnen we met een skelet src/my-app.js

En onze test in test/my-app.test.js;

Uitvoeren van de test => mislukt en dan voegen we de implementatie toe aan src/a11y-input.js

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

Maar oh nee! Dat zou nu groen moeten zijn…

Wat gebeurt er?
Weetje nog dat we een specifieke test hadden om de licht-dom van a11y-input te garanderen?
Dus zelfs als de gebruiker alleen <a11y-input></a11y-input> in zijn code zet – wat er in werkelijkheid uitkomt is

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

b.v. a11y-input maakt in feite nodes binnen je my-app schaduw-dom. Belachelijk! Voor ons voorbeeld hier zeggen we dat dat is wat we willen.
Dus hoe kunnen we het toch testen?

Gelukkig heeft .shadowDom nog een troef achter de hand; het stelt ons in staat om delen van dom te negeren.

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

We kunnen zelfs de volgende eigenschappen opgeven:

  • ignoreChildren
  • ignoreTags
  • ignoreAttributes (globaal of voor specifieke tags)

Voor meer details, zie semantic-dom-diff.

Snapshot testing

Als je veel grote dom trees hebt, wordt het lastig om al die handmatig geschreven verwachtingen te schrijven/onderhouden.
Om je daarbij te helpen zijn er semi/automatische snapshots.

Dus als we onze code wijzigen

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

Als we nu npm run test uitvoeren, wordt een bestand __snapshots__/a11y input.md aangemaakt en gevuld met iets als dit

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

Wat we eerder met de hand hebben geschreven, kan nu automatisch worden gegenereerd bij het opstarten of geforceerd via npm run test:update-snapshots.

Als het bestand __snapshots__/a11y input.md al bestaat, wordt het vergeleken met de uitvoer en krijgt u fouten als uw html-uitvoer is veranderd.

Voor meer details, zie semantic-dom-diff.

Ik denk dat het nu genoeg is over het vergelijken van dom trees…
Het is tijd voor een verandering 🤗

Code coverage

Een andere nuttige metric die we krijgen bij het testen met de open-wc setup is code coverage.
Dus wat betekent het en hoe kunnen we het krijgen? Code coverage is een maat voor hoeveel van onze code wordt gecontroleerd door tests. Als er een regel, verklaring, functie of tak is (bijv. if/else verklaring) die niet door onze tests wordt gedekt, wordt onze dekkingsscore beïnvloed.
Een simpele npm run test is alles wat we nodig hebben en je krijgt het volgende:

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

Wat betekent dat 100% van de verklaringen, takken, functies en regels van onze code door tests worden gedekt. Dat is mooi!

Dus laten we de andere kant op gaan en code toevoegen aan src/a11y-input.js voordat we een test toevoegen. Laten we zeggen dat we de waarde van onze invoer rechtstreeks willen benaderen via ons aangepaste element en dat we iets willen loggen wanneer de waarde ‘cat’ is.

Het resultaat is heel anders

Onze dekking is veel lager dan voorheen. Onze testopdracht mislukt zelfs, hoewel alle tests met succes zijn uitgevoerd.
Dit komt omdat de configuratie van open-wc standaard een drempelwaarde van 90% voor codedekking instelt.

Als we de dekking willen verbeteren, moeten we tests toevoegen – dus laten we dat doen

uh oh 😱 we wilden de dekking verbeteren, maar nu moeten we eerst een echte bug oplossen 😞

Dat was onverwacht… op het eerste gezicht weet ik niet echt wat dat betekent… het is beter om een paar echte nodes te controleren en ze in de browser te inspecteren.

Debuggen in de browser

Wanneer we onze test met watch uitvoeren, zet karma een persistente browseromgeving op om tests in uit te voeren.

  • Zorg ervoor dat u gestart bent met npm run test:watch
  • visit http://localhost:9876/debug.html

U zou zoiets als dit moeten zien

U kunt op de omcirkelde play knop klikken om alleen een individuele test uit te voeren.

Dus laten we de Chrome Dev Tools (F12) openen en een debugger in de testcode plaatsen.

Dang… de fout treedt al voor dat punt op…
“Fatale” fouten zoals deze zijn een beetje lastiger, omdat het geen falende tests zijn, maar een soort volledige ineenstorting van uw volledige component.

Ok, laten we wat code direct in de setter plaatsen.

set value(newValue) { debugger;

Al goed, dat werkte dus in onze chrome console schrijven we console.log(this) laten we eens kijken wat we hier hebben

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

Ahh daar hebben we het – de schaduw-dom is nog niet gerenderd wanneer de setter wordt aangeroepen.
Dus laten we het zekere voor het onzekere nemen en een controle toevoegen

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

Fatel fout is weg 🎉
Maar we hebben nu een falende test 😭

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

We hebben misschien een andere tactiek nodig 🤔
We kunnen het toevoegen als een aparte value eigenschap en synchroniseren wanneer nodig.

En we zijn eindelijk terug in zaken! 🎉

ok bug gerepareerd – kunnen we alsjeblieft terug naar de dekking? dank je 🙏

Terug naar de dekking

Met deze toegevoegde test hebben we enige vooruitgang geboekt.

Hoewel we er nog niet helemaal zijn – de vraag is waarom?

Om daar achter te komen open coverage/index.html in je browser. Geen webserver nodig, open gewoon het bestand in je browser – op een mac kun je dat vanaf de opdrachtregel doen met open coverage/index.html

Je ziet dan zoiets als dit

Als je op a11y-input.js klikt, krijg je regel voor regel te zien hoe vaak ze zijn uitgevoerd.
Zo kunnen we meteen zien welke regels nog niet zijn uitgevoerd door onze tests.

Dus laten we daar een test voor toevoegen

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

Met dat, zijn we terug op 100% op statements maar we missen nog steeds iets op branches.
Laten we eens kijken waarom?

Dit E betekent else path not taken.
Dus telkens als de functie update wordt aangeroepen is er altijd een eigenschap value in de gewijzigdeEigenschappen.

We hebben ook label dus het is een goed idee om het te testen. 👍

boom 100% 💪 we winnen 🥇

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

Maar wacht we hebben de test hierboven niet eens afgemaakt – de code is nog steeds

 // somehow check that console.log was called

Hoe komt het dat we 100% testdekking hebben?

Laten we eerst proberen te begrijpen hoe code coverage werkt 🤔
De manier waarop code coverage wordt gemeten is door een vorm van instrumentation toe te passen. Kort gezegd, voordat onze code wordt uitgevoerd, wordt deze gewijzigd (instrumented) en gedraagt deze zich ongeveer als volgt:

Note: dit is een sterk vereenvoudigde versie ter illustratie.

Basically, your code gets littered with many many flags. Op basis van welke vlaggen worden geactiveerd, wordt een statistiek gemaakt.

Dus 100% testdekking betekent alleen dat elke regel in je code ten minste één keer is uitgevoerd nadat al je tests zijn voltooid. Het betekent niet dat je alles getest hebt, of dat je tests de juiste assertions maken.

Dus ook al hebben we al 100% code coverage we gaan nog steeds onze log test verbeteren.

Je moet code coverage daarom zien als een hulpmiddel dat je alleen maar houvast en hulp geeft bij het spotten van enkele ontbrekende tests, en niet als een harde garantie voor de kwaliteit van de code.

Code bespioneren

Als je wilt controleren hoe vaak of met welke parameters een functie wordt aangeroepen, heet dat spioneren.
open-wc beveelt het eerbiedwaardige sinon-pakket aan, dat veel hulpmiddelen biedt voor spionage en andere gerelateerde taken.

npm i -D sinon

Dus je maakt een spion voor een specifiek object en dan kun je controleren hoe vaak het wordt aangeroepen.

Uh oh… de test mislukt:

AssertionError: expected 0 to equal 1

Het werken met globale objecten zoals console kan neveneffecten hebben, dus laten we beter een speciale logfunctie gebruiken.

Dit resulteert in geen globaal object in onze testcode – lief 🤗

Hoewel we nog steeds dezelfde foutmelding krijgen. Laten we debuggen… boohoo blijkbaar is update niet sync – een verkeerde veronderstelling die ik maakte 🙈 Ik zeg dat veronderstellingen nogal eens gevaarlijk zijn – toch trap ik er van tijd tot tijd in 😢.

Dus wat kunnen we doen? Helaas lijkt er geen publieke api te zijn om sommige sync acties te doen die getriggerd worden door een eigenschap update.
Laten we er een issue voor maken https://github.com/Polymer/lit-element/issues/643.

Voorlopig is blijkbaar de enige manier te vertrouwen op een private api. 🙈
Ook moesten we de value sync naar updated verplaatsen, zodat deze na elke dom render wordt uitgevoerd.

en hier is de bijgewerkte test voor de logging

wauw, dat was een beetje moeilijker dan verwacht, maar het is ons gelukt 💪

SUMMARY:✔ 7 tests completedTOTAL: 7 SUCCESS

Tests uitvoeren zonder Karma Framework

Het Karma framework is krachtig en rijk aan functies, maar soms willen we ons testregime misschien wat inkorten. Het mooie van alles wat we tot nu toe hebben voorgesteld is dat we alleen browser-standaard es modules hebben gebruikt zonder noodzaak voor transpilatie, met de enige uitzondering van kale modulespecificaties.
Dus gewoon door een test/index.html.

en het te openen via owc-dev-server in chrome, zal het prima werken.
We hebben alles aan de praat gekregen zonder webpack of karma – lief 🤗

Doe het Cross-Browser ding

We voelen ons nu behoorlijk comfortabel met ons web component. Het is getest en gedekt; er is nog maar één stap – we willen er zeker van zijn dat het in alle browsers draait en getest is.

Dus laten we het gewoon draaien

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

Ja, dat werkt mooi! 🤗

Als er tests mislukken, worden deze in de samenvatting weergegeven met de specifieke browser waar de test is mislukt.

Als u een bepaalde browser wilt debuggen:

Als u de browser wilt aanpassen die wordt getest, kunt u uw karma.bs.config.js aanpassen.

Wilt u bijvoorbeeld de Firefox ESR aan uw lijst toevoegen.

Of misschien wilt u slechts 2 specifieke browsers testen?

Note: dit maakt gebruik van de webpack merge strategies replace.

Quick Recap

  • Testen is belangrijk voor elk project. Zorg ervoor dat u er zoveel mogelijk schrijft.
  • Probeer uw code coverage hoog te houden, maar onthoud dat het geen magische garantie is, dus het hoeft niet altijd 100% te zijn.
  • Debug in de browser via npm run test:watch. Voor oudere browsers gebruikt u npm run test:legacy.watch.

What’s Next?

  • Run de tests in uw CI (werkt perfect in combinatie met browserstack). Zie onze aanbevelingen bij automatiseren.

Volg ons op Twitter, of volg mij op mijn persoonlijke Twitter.
Zorg ervoor dat je onze andere tools en aanbevelingen op open-wc.org bekijkt.

Dank aan Pascal en Benny voor hun feedback en hulp om mijn gekrabbel om te zetten in een volgbaar verhaal.