Testarea fluxului de lucru pentru componente web
Când livrați ceva pentru a fi folosit de alții, vă asumați responsabilitatea de a livra un cod sigur și stabil. O modalitate de a aborda acest lucru este testarea codului dumneavoastră.
Nu contează cât de mic – nu contează cât de simplu este proiectul dumneavoastră, în mod ideal ar trebui să existe întotdeauna teste.
Da, știu că realitatea lovește dur și vor exista multe cazuri în care acest lucru nu se va întâmpla – dar ar trebui să vă străduiți întotdeauna să aveți teste
Disclaimer
În acest tutorial, vom realiza o versiune simplă a unui element de intrare. Până la sfârșitul acestuia, veți dobândi abilitățile și cunoștințele necesare pentru a pune în practică instrumentele de testare open-wc; și veți construi o componentă de intrare solidă, accesibilă și bine testată.
Avertisment
Acest tutorial este un tutorial aprofundat care prezintă câteva capcane și cazuri dificile atunci când se lucrează cu componente web. Acesta este cu siguranță pentru utilizatorii mai avansați. Ar trebui să aveți cunoștințe de bază despre LitElement și tipurile JSDoc. Să aveți o idee despre ce este Mocha, Chai BDD, Karma ar putea ajuta puțin, de asemenea.
Ne gândim să postăm o versiune mai ușor de digerat a acestui lucru, așa că, dacă este ceva ce ați dori să vedeți – anunțați-ne în comentarii.
Dacă doriți să vă jucați împreună – tot codul este pe github.
Să începem!
Executați în consolă
$ 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
Pentru mai multe detalii vă rugăm să vedeți https://open-wc.org/testing/.
Ștergeți src/A11yInput.js
Modificați src/a11y-input.js
în:
și test/a11y-input.test.js
în:
Testele noastre de până acum constau dintr-o singură caracteristică (proprietatea label
) și o singură afirmație (expect
). Folosim sintaxa BDD de la karma și chai, așa că grupăm seturile de teste (it
) sub caracteristicile sau API-urile la care se referă (describe
).
Să vedem dacă totul funcționează corect prin rularea: npm run test
.
Frumos – așa cum era de așteptat (🥁), avem un test eșuat 🙂
Să trecem în modul de supraveghere, care va rula testele în mod continuu ori de câte ori veți face modificări în cod.
npm run test:watch
Codul următor a fost adăugat în videoclipul de mai sus la src/a11y-input.js
:
static get properties() { return { label: { type: String }, };}constructor() { super(); this.label = '';}
Așa de departe, totul e bine? Încă mai sunteți cu noi? Minunat! Să ridicăm puțin nivelul jocului…
Adăugarea unui test pentru Shadow DOM
Să adăugăm o aserțiune pentru a testa conținutul rădăcinii de umbră a elementului nostru.
Dacă vrem să fim siguri că elementul nostru se comportă/ arată la fel, ar trebui să ne asigurăm că structura sa de dom rămâne aceeași.
Așa că haideți să comparăm domul de umbră actual cu ceea ce vrem să fie.
Cum era de așteptat, obținem:
Așa că haideți să implementăm asta în elementul nostru.
render() { return html` <slot name="label"></slot> <slot name="input"></slot> `;}
Interesant, testul ar trebui să fie verde… dar nu este 🤔 Să aruncăm o privire.
Poate că ați observat acele tag-uri ciudate de comentarii goale <!---->
. Acestea sunt markeri pe care lit-html
îi folosește pentru a-și aminti unde se află părțile dinamice, astfel încât să poată fi actualizate eficient. Pentru testare, totuși, acest lucru poate fi un pic enervant de gestionat.
Dacă am folosi innerHTML
pentru a compara DOM, ar trebui să ne bazăm pe simpla egalitate a șirurilor de caractere. În aceste condiții, ar trebui să ne potrivim exact cu spațiile albe, comentariile, etc. din DOM-ul generat; cu alte cuvinte: va trebui să fie o potrivire perfectă. De fapt, tot ce trebuie să testăm este că elementele pe care dorim să le redăm sunt redate. Vrem să testăm conținutul semantic al rădăcinii de umbră.
Așa că hai să încercăm 💪
// 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
Cum funcționează shadowDom.to.equal()?
- Obține
innerHTML
al rădăcinii de umbră - Îl interpretează (de fapt, browserul o analizează – nu este nevoie de nicio bibliotecă)
- Normalizează (potențial fiecare tag/proprietate pe propria linie)
- Analizează și normalizează șirul HTML așteptat
- Pasează ambele șiruri DOM normalizate către funcția implicită de comparare a lui chai
- În caz de eșec, grupează și afișează orice diferențe într-o manieră clară
Dacă doriți să aflați mai multe, vă rugăm să consultați documentația lui semantic-dom-diff.
Testarea DOM „light”
Potem face exact același lucru cu DOM light. (DOM-ul care va fi furnizat de utilizatorul nostru sau de valorile noastre implicite, adică children
ale elementului).
Și haideți să îl implementăm.
Așa că am testat domul nostru de lumină și umbră 💪 și testele noastre rulează curat 🎉
Nota: Utilizarea API-ului DOM în ciclul de viață al unui element lit este un anti-pattern, totuși, pentru a permite a11y ar putea fi un caz real de utilizare – în orice caz, este grozav în scop ilustrativ
Utilizarea elementului nostru într-o aplicație
Acum că avem o intrare a11y de bază, haideți să o folosim – și să o testăm – în aplicația noastră.
Încă o dată începem cu un schelet src/my-app.js
Și testul nostru din test/my-app.test.js
;
Executarea testului => eșuează și apoi adăugăm implementarea la src/a11y-input.js
render() { return html` <h1>My Filter App</h1> <a11y-input></a11y-input> `;}
Dar oh, nu! Ar trebui să fie verde acum…
Ce se întâmplă?
Îți amintești că am avut un test specific pentru a asigura light-domul a11y-input?
Așa că, chiar dacă utilizatorii pun doar <a11y-input></a11y-input>
în codul său – ceea ce iese de fapt este
<a11y-input> <label slot="label"></label> <input slot="input"></a11y-input>
de exemplu, a11y-input
creează de fapt noduri în interiorul domului tău de umbră my-app
. Absurd! Pentru exemplul nostru de aici spunem că asta este ceea ce vrem.
Atunci cum putem totuși să o testăm?
Din fericire .shadowDom
are un alt as în mânecă; ne permite să ignorăm părți din dom.
expect(el).shadowDom.to.equal(` <h1>My Filter App</h1> <a11y-input></a11y-input>`, { ignoreChildren: });
Potem chiar specifica și următoarele proprietăți:
ignoreChildren
ignoreTags
-
ignoreAttributes
(global sau pentru anumite etichete)
Pentru mai multe detalii, vă rugăm să consultați semantic-dom-diff.
Testarea instantaneelor
Dacă aveți o mulțime de arbori de domenii mari scrierea/întreținerea tuturor acelor așteptări scrise manual va fi dificilă.
Pentru a vă ajuta în acest sens există instantaneele semi/automate.
Dacă ne schimbăm codul
// fromexpect(el).shadowDom.to.equal(` <slot name="label"></slot> <slot name="input"></slot>`);// toexpect(el).shadowDom.to.equalSnapshot();
Dacă executăm acum npm run test
acesta va crea un fișier __snapshots__/a11y input.md
și îl va umple cu ceva de genul acesta
# `a11y input`#### `has a static shadowDom```html<slot name="label"></slot><slot name="input"></slot>``
Ceea ce am scris înainte manual poate fi acum generat automat la init sau forțat prin npm run test:update-snapshots
.
Dacă fișierul __snapshots__/a11y input.md
există deja, îl va compara cu ieșirea și veți primi erori dacă ieșirea html s-a schimbat.
Pentru mai multe detalii vă rugăm să consultați semantic-dom-diff.
Cred că acum este suficient despre compararea arborilor de domenii…
Este timpul pentru o schimbare 🤗
Acoperirea codului
O altă măsurătoare utilă pe care o obținem atunci când testăm cu configurația open-wc este acoperirea codului.
Acum ce înseamnă și cum o putem obține? Acoperirea codului este o măsură a cât de mult din codul nostru este verificat de teste. Dacă există o linie, declarație, funcție sau ramură (de exemplu, declarația if
/else
) pe care testele noastre nu o acoperă, scorul nostru de acoperire va fi afectat.
Un simplu npm run test
este tot ce avem nevoie și veți obține următoarele:
=============================== Coverage summary ===============================Statements : 100% ( 15/15 )Branches : 100% ( 0/0 )Functions : 100% ( 5/5 )Lines : 100% ( 15/15 )================================================================================
Ceea ce înseamnă că 100% din declarațiile, ramurile, funcțiile și liniile codului nostru sunt acoperite de teste. Destul de frumos!
Așa că haideți să mergem în cealaltă direcție și să adăugăm cod la src/a11y-input.js
înainte de a adăuga un test. Să presupunem că vrem să accesăm valoarea input-ului nostru direct prin intermediul elementului nostru personalizat și ori de câte ori valoarea sa este ‘cat’ vrem să înregistrăm ceva.
Este un rezultat foarte diferit
Acoperirea noastră este mult mai mică decât înainte. Comanda noastră de testare chiar eșuează, chiar dacă toate testele se execută cu succes.
Acest lucru se datorează faptului că, în mod implicit, configurația open-wc stabilește un prag de 90% pentru acoperirea codului.
Dacă vrem să îmbunătățim acoperirea trebuie să adăugăm teste – așa că haideți să o facem
uh oh 😱 am vrut să îmbunătățim acoperirea, dar acum trebuie să rezolvăm mai întâi un bug real 😞
Acesta a fost neașteptat… la prima vedere, nu prea știu ce înseamnă asta… mai bine să verificăm niște noduri reale și să le inspectăm în browser.
Depanarea în browser
Când rulăm testul nostru cu watch, karma stabilește un mediu de browser persistent în care să rulăm testele.
- Asigurați-vă că ați început cu
npm run test:watch
- vizitați http://localhost:9876/debug.html
Ar trebui să vedeți ceva de genul acesta
Puteți face clic pe butonul de redare încercuit pentru a rula doar un test individual.
Așa că haideți să deschidem Chrome Dev Tools (F12) și să punem un depanator în codul testului.
Dang… eroarea apare chiar înainte de acest punct…
Erorile „fatale” ca aceasta sunt puțin mai dificile, deoarece nu sunt teste eșuate, ci un fel de topire completă a întregii componente.
Ok, haideți să punem niște cod în setter
direct.
set value(newValue) { debugger;
În regulă, asta a funcționat, așa că în consola noastră chrome scriem console.log(this)
să vedem ce avem aici
<a11y-input> #shadow-root (open)</a11y-input>
Ah, iată-l – domul de umbră nu este încă redat atunci când este apelat setterul.
Așa că haideți să fim siguri și să adăugăm o verificare înainte
set value(newValue) { if (newValue === 'cat') { console.log('We like cats too :)'); } if (this.inputEl) { this.inputEl.value = newValue; }}
Eroarea Fatel a dispărut 🎉
Dar acum avem un test eșuat 😭
✖ can set/get the input value directly via the custom elementAssertionError: expected '' to equal 'foo'
Am putea avea nevoie de o schimbare de tactică 🤔
Puteți să o adăugăm ca o proprietate value
separată și să o sincronizăm când este nevoie.
Și, în sfârșit, suntem din nou în activitate! 🎉
ok bug fixat – putem, vă rog, să ne întoarcem la acoperire? mulțumesc 🙏
Înapoi la acoperire
Cu acest test adăugat am făcut unele progrese.
Cu toate acestea, încă nu suntem pe deplin acolo – întrebarea este de ce?
Pentru a afla deschideți coverage/index.html
în browserul dumneavoastră. Nu este nevoie de un server web, doar deschideți fișierul în browser – pe un mac puteți face asta din linia de comandă cu open coverage/index.html
Vă veți vedea ceva de genul acesta
După ce dați click pe a11y-input.js
veți obține o informație linie cu linie cât de des au fost executate.
Așa putem vedea imediat ce linii nu sunt încă executate de testele noastre.
Așa că haideți să adăugăm un test pentru asta
=============================== Coverage summary ===============================Statements : 100% ( 24/24 )Branches : 75% ( 3/4 )Functions : 100% ( 7/7 )Lines : 100% ( 24/24 )================================================================================
Cu asta, suntem din nou la 100% pe declarații, dar încă ne lipsește ceva pe ramuri.
Să vedem de ce?
Acest E
înseamnă else path not taken
.
Atunci, ori de câte ori funcția update
este apelată, există întotdeauna o proprietate value
în changedProperties.
Avem și label
deci este o idee bună să o testăm. 👍
boom 100% 💪 am câștigat 🥇
=============================== Coverage summary ===============================Statements : 100% ( 24/24 )Branches : 100% ( 4/4 )Functions : 100% ( 7/7 )Lines : 100% ( 24/24 )================================================================================
Dar stați că nici măcar nu am terminat testul de mai sus – codul este încă
// somehow check that console.log was called
Cum se face că avem 100% acoperire de test?
Să încercăm mai întâi să înțelegem cum funcționează acoperirea codului 🤔
Modul în care se măsoară acoperirea codului este prin aplicarea unei forme de instrumentation
. Pe scurt, înainte ca codul nostru să fie executat, acesta este modificat (instrumented
) și se comportă cam așa:
Nota: Aceasta este o versiune super simplificată în scop ilustrativ.
În principiu, codul tău este presărat cu multe multe multe stegulețe. În funcție de ce stegulețe se declanșează se creează o statistică.
Așa că o acoperire de testare de 100% înseamnă doar că fiecare linie pe care o aveți în cod a fost executată cel puțin o dată după ce toate testele dvs. s-au terminat. Nu înseamnă că ați testat totul sau dacă testele dvs. fac afirmațiile corecte.
Așa că, chiar dacă avem deja o acoperire de 100% a codului, tot vom îmbunătăți testul nostru de log.
Ar trebui, prin urmare, să vedeți acoperirea codului ca pe un instrument care vă oferă doar îndrumare și ajutor în depistarea unor teste lipsă, mai degrabă decât o garanție fermă a calității codului.
Spionarea codului
Dacă doriți să verificați cât de des sau cu ce parametri se numește o funcție, asta se numește spionare.
open-wc recomandă venerabilul pachet sinon, care oferă multe instrumente pentru spionare și alte sarcini conexe.
npm i -D sinon
Așa că creați un spion pe un anumit obiect și apoi puteți verifica cât de des este apelat.
Uh oh… testul eșuează:
AssertionError: expected 0 to equal 1
Mersul cu obiecte globale cum ar fi console
ar putea avea efecte secundare, așa că mai bine să refactorizăm folosind o funcție log dedicată.
Aceasta duce la lipsa unui obiect global în codul nostru de test – drăguț 🤗
Cu toate acestea, primim în continuare aceeași eroare. Să depanăm… boohoo se pare că update
nu este sincronizat – o presupunere greșită pe care am făcut-o 🙈 Spun că presupunerile sunt periculoase destul de des – totuși mai cad din când în când în capcană 😢.
Atunci ce putem face? Din păcate, se pare că nu există o api publică pentru a face unele acțiuni de sincronizare declanșate de o actualizare a proprietății.
Să creăm o problemă pentru asta https://github.com/Polymer/lit-element/issues/643.
Pentru moment, se pare că singura cale este să ne bazăm pe o api privată. 🙈
De asemenea, trebuia să mutăm sincronizarea valorilor în updated
, astfel încât să fie executată după fiecare redare a domului.
și iată testul actualizat pentru logare
wow, a fost puțin mai greu decât ne așteptam, dar am reușit 💪
SUMMARY:✔ 7 tests completedTOTAL: 7 SUCCESS
Executarea testelor fără cadrul Karma
Cadrul Karma este puternic și bogat în funcționalități, dar uneori am putea dori să reducem regimentul nostru de testare. Lucrul bun cu tot ceea ce am propus până acum este că am folosit doar modulele es standard ale browserului, fără a fi nevoie de transpilare, cu o singură excepție, cea a specificatorilor de module goale.
Așa că doar prin crearea unui test/index.html
.
și deschizându-l prin owc-dev-server
în chrome, acesta va funcționa perfect.
Am reușit să facem totul să funcționeze fără webpack
sau karma
– dulce 🤗
Fă lucrul între browsere
Acum ne simțim destul de confortabil cu componenta noastră web. Este testată și acoperită; mai este doar un singur pas – vrem să ne asigurăm că rulează și este testată în toate browserele.
Așa că haideți să o rulăm
npm run test:bsSUMMARY:✔ 42 tests completedTOTAL: 42 SUCCESS
Da, funcționează foarte bine! 🤗
Dacă există teste eșuate, le va afișa în rezumat cu browserul specific în care a eșuat.
Dacă aveți nevoie să depanați un anumit browser:
De asemenea, dacă doriți să ajustați browserul care este testat, puteți ajusta karma.bs.config.js
.
De exemplu, dacă doriți să adăugați Firefox ESR
la lista dvs.
Ou poate doriți să testați doar 2 browsere specifice?
Nota: Acest lucru utilizează strategiile de fuziune webpack merge replace.
Recapitulare rapidă
- Testarea este importantă pentru fiecare proiect. Asigurați-vă că scrieți cât mai multe pe cât puteți.
- Încercați să mențineți acoperirea codului la un nivel ridicat, dar nu uitați că nu este o garanție magică, așa că nu trebuie să fie întotdeauna 100%.
- Debug în browser prin
npm run test:watch
. Pentru browserele moștenite, folosiținpm run test:legacy.watch
.
Ce urmează?
- Executați testele în CI-ul dvs. (funcționează perfect împreună cu browserstack). Vedeți recomandările noastre la automatizare.
Să ne urmăriți pe Twitter, sau urmăriți-mă pe Twitter-ul meu personal.
Asigură-te că verifici și celelalte instrumente și recomandări de la open-wc.org.
Mulțumesc lui Pascal și Benny pentru feedback și pentru că m-au ajutat să transform mâzgăliturile mele într-o poveste urmăribilă.
.