Articles

Arbetsflöde för testning av webbkomponenter

När du levererar något som ska användas av andra tar du ansvar för att leverera säker och stabil kod. Ett sätt att ta itu med detta är att testa din kod.

Oavsett hur litet – oavsett hur enkelt ditt projekt är, bör det alltid helst finnas tester.

Ja, jag vet att verkligheten slår hårt och att det kommer att finnas många fall där det inte händer – men du bör alltid sträva efter att ha tester

Ansvarsfriskrivning

I den här handledningen ska vi göra en enkel version av ett inmatningselement. I slutet av den kommer du att få färdigheter och kunskaper för att använda open-wc-testverktyg i praktiken; och bygga en solid, tillgänglig och vältestad inmatningskomponent.

Varning

Det här är en djupgående handledning som visar några fallgropar och svåra fall när man arbetar med webbkomponenter. Detta är definitivt för mer avancerade användare. Du bör ha grundläggande kunskaper om LitElement och JSDoc-typer. Att ha en aning om vad Mocha, Chai BDD, Karma är kan hjälpa lite också.

Vi funderar på att publicera en mer lättsmält version av detta så om det är något du skulle vilja se – låt oss veta i kommentarerna.

Om du vill spela med – all kod finns på github.

Nu sätter vi igång!

Kör i din konsol

$ 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 mer information se https://open-wc.org/testing/.

Släpp src/A11yInput.js

Modifiera src/a11y-input.js till:

och test/a11y-input.test.js till:

Våra tester består hittills av en enda funktion (egenskapen label) och ett enda påstående (expect). Vi använder karma och chais BDD-syntax, så vi grupperar uppsättningar av tester (it) under de funktioner eller API:er som de relaterar till (describe).

Låt oss se om allt fungerar korrekt genom att köra: npm run test.

Grymt – precis som förväntat (🥁) har vi ett misslyckat test 🙂

Låt oss gå över till bevakningsläge, vilket kommer att köra testerna kontinuerligt när du gör ändringar i din kod.

npm run test:watch

Följande kod lades till i videon ovan till src/a11y-input.js:

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

Så långt, så bra? Är du fortfarande med oss? Bra! Låt oss höja spelet lite…

Lägga till ett test för Shadow DOM

Låt oss lägga till en assertion för att testa innehållet i vårt elements skuggrot.

Om vi vill vara säkra på att vårt element beter sig/ser likadant ut bör vi se till att dess dom-struktur förblir densamma.
Så låt oss jämföra den faktiska shadow dom med vad vi vill att den ska vara.

Som väntat får vi:

Så låt oss implementera det i vårt element.

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

Interessant, testet borde vara grönt… men det är det inte 🤔 Låt oss ta en titt.

Du kanske har lagt märke till de där konstiga tomma kommentaren <!----> taggarna. De är markörer som lit-html använder för att komma ihåg var dynamiska delar finns, så att den kan uppdateras effektivt. För testning kan detta dock vara lite irriterande att hantera.

Om vi använder innerHTML för att jämföra DOM måste vi förlita oss på enkel strängjämlikhet. Under dessa omständigheter måste vi exakt matcha den genererade DOM:s vitrymder, kommentarer etc., med andra ord: det måste vara en perfekt matchning. Egentligen är allt vi behöver testa att de element som vi vill återge är återgivna. Vi vill testa det semantiska innehållet i skuggroten.

Så låt oss prova 💪

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

Hur fungerar shadowDom.to.equal()?

  1. Det hämtar innerHTML för skuggroten
  2. Parses it (actually, webbläsaren analyserar den – inget bibliotek behövs)
  3. Normaliserar den (potentiellt varje tagg/egenskap på en egen rad)
  4. Parserar och normaliserar den förväntade HTML-strängen
  5. Passerar båda de normaliserade DOM-strängarna vidare till chai’s standardjämförelsefunktion
  6. I händelse av misslyckande, grupperar och visar eventuella skillnader på ett tydligt sätt

Om du vill veta mer kan du kolla in dokumentationen för semantic-dom-diff.

Testa det ”lätta” DOM

Vi kan göra exakt samma sak med det lätta DOM. (Den DOM som kommer att tillhandahållas av vår användare eller våra standardvärden, dvs. elementens children).

Och låt oss implementera det.

Så vi testade vår ljus- och skuggdom 💪 och våra tester går rent 🎉

Note: Men för att möjliggöra a11y kan det vara ett verkligt användningsområde – hur som helst är det bra för illustrationsändamål

Användning av vårt element i en applikation

Så nu när vi har en grundläggande a11y-inmatning ska vi använda den – och testa den – i vår applikation.

Även vi börjar med ett skelett src/my-app.js

Och vårt test i test/my-app.test.js;

Kör testet => misslyckas och sedan lägger vi till implementationen i src/a11y-input.js

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

Men åh nej! Det borde vara grönt nu…

Vad är det som händer?
Har du kommit ihåg att vi hade ett specifikt test för att säkerställa ljusdom av a11y-input?
Så även om användaren bara lägger in <a11y-input></a11y-input> i sin kod – vad som faktiskt kommer ut är

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

dvs. a11y-input skapar faktiskt noder inne i din my-app shadow dom. Det är absurt! För vårt exempel här säger vi att det är vad vi vill ha.
Så hur kan vi ändå testa det?

Turligtvis har .shadowDom ett annat ess i rockärmen; det tillåter oss att ignorera delar av dom.

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

Vi kan även specificera följande egenskaper också:

  • ignoreChildren
  • ignoreTags
  • ignoreAttributes (globalt eller för specifika taggar)

För mer detaljer se semantic-dom-diff.

Snapshot testing

Om du har många stora dom-träd kommer det att vara svårt att skriva/underhålla alla dessa manuellt skrivna expects.
För att hjälpa dig med det finns det halv/automatiska snapshots.

Så om vi ändrar vår kod

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

Om vi nu exekverar npm run test kommer den att skapa en fil __snapshots__/a11y input.md och fylla den med något som liknar detta

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

Det som vi tidigare skrev för hand kan nu automatiskt genereras vid init eller forcerat via npm run test:update-snapshots.

Om filen __snapshots__/a11y input.md redan finns kommer den att jämföra den med utdata och du kommer att få fel om din html-utdata ändras.

För mer information se semantic-dom-diff.

Jag tror att det räcker nu med att jämföra dom-träd…
Det är dags för en förändring 🤗

Kodtäckning

Ett annat användbart mått som vi får när vi testar med open-wc-uppsättningen är kodtäckning.
Så vad betyder det och hur kan vi få det? Kodtäckning är ett mått på hur stor del av vår kod som kontrolleras av tester. Om det finns en rad, ett uttalande, en funktion eller en gren (t.ex. if/else uttalande) som våra tester inte täcker kommer vår täckningsgrad att påverkas.
En enkel npm run test är allt vi behöver och du får följande:

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

Vad betyder att 100 % av vår kods uttalanden, grenar, funktioner och rader är täckta av tester. Ganska snyggt!

Så låt oss gå åt andra hållet och lägga till kod till src/a11y-input.js innan vi lägger till ett test. Låt oss säga att vi vill få tillgång till värdet på vår ingång direkt via vårt anpassade element och när dess värde är ”cat” vill vi logga något.

Det är ett helt annat resultat

Vår täckning är mycket lägre än tidigare. Vårt testkommando misslyckas till och med, trots att alla tester körs framgångsrikt.
Detta beror på att open-wc:s config som standard anger ett tröskelvärde på 90 % för kodtäckning.

Om vi vill förbättra täckningen måste vi lägga till tester – så låt oss göra det

uh oh 😱 vi ville förbättra täckningen men nu måste vi fixa ett faktiskt fel först 😞

Det där var oväntat… vid första anblicken vet jag inte riktigt vad det betyder… bättre att kontrollera några faktiska noder och inspektera dem i webbläsaren.

Felsökning i webbläsaren

När vi kör vårt test med watch ställer karma in en beständig webbläsarmiljö för att köra tester i.

  • Se till att du började med npm run test:watch
  • besök http://localhost:9876/debug.html

Du bör se något som liknar detta

Du kan klicka på den inringade play-knappen för att bara köra ett enskilt test.

Så låt oss öppna Chrome Dev Tools (F12) och sätta en debugger i testkoden.

Dang… felet inträffar till och med före den punkten…
”Fatala” fel som detta är lite svårare eftersom de inte misslyckas med testerna utan är ett slags fullständig härdsmälta av hela komponenten.

Okej, låt oss sätta in lite kod i setter direkt.

set value(newValue) { debugger;

Okej, det fungerade så vår Chrome-konsol skriver vi console.log(this) låt oss se vad vi har här

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

Ahh där har vi det – shadow dom är ännu inte renderad när settern anropas.
Så låt oss vara säkra och lägga till en kontroll innan

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

Felfelet är borta 🎉
Men vi har nu ett misslyckat test 😭

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

Vi kan behöva ändra taktik 🤔
Vi kan lägga till det som en separat valueegenskap och synkronisera vid behov.

Och vi är äntligen igång igen! 🎉

ok bugg fixad – kan vi återgå till täckningen? tack 🙏

Tillbaka till täckningen

Med det här tillagda testet har vi gjort vissa framsteg.

Hur som helst har vi fortfarande inte nått hela vägen fram – frågan är varför?

För att ta reda på det öppnar du coverage/index.html i din webbläsare. Ingen webbserver behövs bara öppna filen i din webbläsare – på en Mac kan du göra det från kommandoraden med open coverage/index.html

Du kommer att se något som liknar detta

När du klickar på a11y-input.js får du information om hur ofta de exekveras rad för rad.
Så vi kan genast se vilka rader som inte har exekverats ännu av våra tester.

Så låt oss lägga till ett test för det

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

Med det är vi tillbaka på 100 % på uttalanden men vi har fortfarande något som saknas på grenar.
Låt oss se varför?

Detta E betyder else path not taken.
Så när funktionen update blir anropad finns det alltid en egenskap value i changedProperties.

Vi har label också så det är en bra idé att testa det. 👍

boom 100% 💪 vi vinner 🥇

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

Men vänta, vi avslutade inte ens testet ovan – koden är fortfarande

 // somehow check that console.log was called

Hur kommer det sig att vi har 100% testtäckning?

Låt oss först försöka förstå hur kodtäckning fungerar 🤔
Sättet som kodtäckning mäts på är genom att tillämpa en form av instrumentation. Kort sagt, innan vår kod exekveras ändras den (instrumented) och det beter sig ungefär så här:

Notera: Detta är en superförenklad version för illustrationsändamål.

I grund och botten blir din kod nedskräpad med många många flaggor. Baserat på vilka flaggor som utlöses skapas en statistik.

Så 100 % testtäckning innebär bara att varje rad i din kod utfördes minst en gång efter att alla dina tester avslutats. Det betyder inte att du har testat allting eller att dina tester gör rätt påståenden.

Så även om vi redan har 100 % kodtäckning kommer vi fortfarande att förbättra vårt loggtest.

Du bör därför se kodtäckning som ett verktyg som bara ger dig vägledning och hjälp med att upptäcka några saknade tester, snarare än en hård garanti för kodens kvalitet.

Spionera på kod

Om du vill kontrollera hur ofta eller med vilka parametrar en funktion kallas kallas kallas det för att spionera.
open-wc rekommenderar det anrika sinon-paketet, som tillhandahåller många verktyg för spionage och andra relaterade uppgifter.

npm i -D sinon

Så du skapar en spion på ett specifikt objekt och sedan kan du kontrollera hur ofta det blir anropat.

Uh oh… testet misslyckas:

AssertionError: expected 0 to equal 1

Med globala objekt som console kan det få bieffekter så låt oss hellre refaktorisera genom att använda en dedikerad loggfunktion.

Detta resulterar i att det inte finns något globalt objekt i vår testkod – snällt 🤗

Hur som helst får vi fortfarande samma fel. Låt oss felsöka… boohoo tydligen är update inte synkroniserad – ett felaktigt antagande jag gjorde 🙈 Jag säger att antaganden är farliga ganska ofta – ändå faller jag för det då och då 😢.

Så vad kan vi göra? Tyvärr verkar det inte finnas något offentligt api för att göra vissa synkroniseringsåtgärder som utlöses av en fastighetsuppdatering.
Låt oss skapa ett ärende för det https://github.com/Polymer/lit-element/issues/643.

För tillfället är tydligen det enda sättet att förlita sig på ett privat api. 🙈
Och vi behövde flytta värdesynkroniseringen till updated så att den exekveras efter varje dom-rendering.

och här är det uppdaterade testet för loggning

Wow, det var lite tuffare än väntat men vi klarade det 💪

SUMMARY:✔ 7 tests completedTOTAL: 7 SUCCESS

Kör tester utan Karma Framework

Karma Framework är kraftfullt och funktionsrikt, men ibland kanske vi vill reducera vårt testregement. Det fina med allt vi föreslagit hittills är att vi bara använt webbläsarstandard es-moduler utan behov av transpilering, med det enda undantaget av bare modules specifiers.
Så bara genom att skapa en test/index.html.

och öppna den via owc-dev-server i chrome kommer den att fungera alldeles utmärkt.
Vi fick allt att fungera utan webpack eller karma – snyggt 🤗

Gör det webbläsaröverskridande

Vi känner oss nu ganska bekväma med vår webbkomponent. Den är testad och täckt; det finns bara ett steg till – vi vill se till att den körs och är testad i alla webbläsare.

Så låt oss bara köra den

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

Ja, det fungerar fint! 🤗

Om det finns misslyckade tester kommer de att visas i sammanfattningen med den specifika webbläsaren där det misslyckades.

Om du behöver felsöka en viss webbläsare:

Också om du vill justera vilken webbläsare som testas kan du justera din karma.bs.config.js.

Till exempel om du vill lägga till Firefox ESR till din lista.

Och kanske du bara vill testa 2 specifika webbläsare?

Notera: Detta använder webpack merge strategies replace.

Snabb sammanfattning

  • Testning är viktigt för varje projekt. Se till att skriva så många som möjligt.
  • Försök att hålla din kodtäckning hög, men kom ihåg att det inte är en magisk garanti, så den behöver inte alltid vara 100 %.
  • Debugga i webbläsaren via npm run test:watch. För äldre webbläsare använder du npm run test:legacy.watch.

What’s Next?

  • Kör testerna i ditt CI (fungerar utmärkt tillsammans med browserstack). Se våra rekommendationer om automatisering.

Följ oss på Twitter, eller följ mig på min personliga Twitter.
Se till att kolla in våra andra verktyg och rekommendationer på open-wc.org.

Tack till Pascal och Benny för feedback och för att ni hjälpte mig att omvandla mitt klotter till en uppföljningsbar historia.