Articles

Flusso di lavoro di test per componenti web

Ogni volta che spedisci qualcosa che sarà usato da altri, ti assumi la responsabilità di consegnare codice sicuro e stabile. Un modo per affrontare questo è testare il tuo codice.

Non importa quanto piccolo – non importa quanto semplice sia il tuo progetto, idealmente ci dovrebbero sempre essere dei test.

Sì, so che la realtà colpisce duro e ci saranno molti casi in cui questo non accade – ma dovresti sempre sforzarti di avere dei test

Disclaimer

In questo tutorial, faremo una semplice versione di un elemento di input. Alla fine di esso, otterrete le abilità e le conoscenze per mettere in pratica gli strumenti di test open-wc; e costruire un componente di input solido, accessibile e ben testato.

Attenzione

Questo è un tutorial approfondito che mostra alcune insidie e casi difficili quando si lavora con componenti web. Questo è sicuramente per utenti più avanzati. Dovreste avere una conoscenza di base di LitElement e JSDoc Types. Avere un’idea di cosa sia Mocha, Chai BDD, Karma potrebbe aiutare un po’.

Stamo pensando di pubblicare una versione più facile da digerire di questo, quindi se è qualcosa che vorreste vedere – fatecelo sapere nei commenti.

Se volete giocare insieme – tutto il codice è su github.

Cominciamo!

Esegui nella tua 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

Per maggiori dettagli vedi https://open-wc.org/testing/.

Cancellare src/A11yInput.js

Modificare src/a11y-input.js in:

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

I nostri test finora consistono in una singola caratteristica (la proprietà label) e una singola asserzione (expect). Stiamo usando la sintassi BDD di karma e chai, quindi raggruppiamo gli insiemi di test (it) sotto le caratteristiche o API a cui si riferiscono (describe).

Vediamo se tutto funziona correttamente eseguendo: npm run test.

Fantastico – proprio come previsto (🥁), abbiamo un test che fallisce 🙂

Passiamo alla modalità watch, che eseguirà i test continuamente ogni volta che si fanno modifiche al codice.

npm run test:watch

Il seguente codice è stato aggiunto nel video sopra a src/a11y-input.js:

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

Fin qui tutto bene? Ancora con noi? Grande! Alziamo un po’ il tiro…

Aggiungere un test per il DOM ombra

Aggiungiamo un’asserzione per testare il contenuto della radice ombra del nostro elemento.

Se vogliamo essere sicuri che il nostro elemento si comporti/abbia lo stesso aspetto dobbiamo assicurarci che la sua struttura dom rimanga la stessa.
Confrontiamo quindi il dom ombra attuale con quello che vogliamo che sia.

Come previsto, otteniamo:

Quindi implementiamo questo nel nostro elemento.

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

Interessante, il test dovrebbe essere verde… ma non lo è 🤔 Diamo un’occhiata.

Avrete notato quegli strani tag vuoti di commento <!---->. Sono marcatori che lit-html usa per ricordare dove sono le parti dinamiche, in modo da poterle aggiornare in modo efficiente. Per i test, tuttavia, questo può essere un po’ fastidioso da gestire.

Se usassimo innerHTML per confrontare il DOM, dovremmo fare affidamento sulla semplice uguaglianza delle stringhe. In queste circostanze, dovremmo far corrispondere esattamente gli spazi bianchi, i commenti, ecc. del DOM generato; in altre parole: dovrà essere una corrispondenza perfetta. In realtà tutto ciò di cui abbiamo bisogno per testare è che gli elementi che vogliamo rendere siano resi. Vogliamo testare il contenuto semantico della radice dell’ombra.

Proviamolo allora 💪

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

Come funziona shadowDom.to.equal()?

  1. Prende il innerHTML della radice dell’ombra
  2. Lo analizza (in realtà, il browser lo analizza – nessuna libreria è necessaria)
  3. Normalizza (potenzialmente ogni tag/proprietà sulla propria linea)
  4. Impara e normalizza la stringa HTML prevista
  5. Passa entrambe le stringhe DOM normalizzate alla funzione di confronto predefinita di chai
  6. In caso di fallimento, raggruppa e visualizza le differenze in modo chiaro

Se vuoi saperne di più, controlla la documentazione di semantic-dom-diff.

Testando il DOM “leggero”

Possiamo fare esattamente la stessa cosa con il DOM leggero. (Il DOM che sarà fornito dal nostro utente o dai nostri default, cioè il children dell’elemento).

E implementiamolo.

Così abbiamo testato il nostro dom luce e ombra 💪 e i nostri test girano puliti 🎉

Nota: Usare l’API DOM nel ciclo di vita di un elemento di luce è un anti-pattern, tuttavia per permettere l’a11y potrebbe essere un caso d’uso reale – comunque è ottimo per scopi illustrativi

Usare il nostro elemento in un’applicazione

Ora che abbiamo un input a11y di base usiamolo – e testiamolo – nella nostra applicazione.

Ancora una volta cominciamo con uno scheletro src/my-app.js

E il nostro test in test/my-app.test.js;

Eseguire il test => fallisce e poi aggiungiamo l’implementazione a src/a11y-input.js

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

Ma oh no! Dovrebbe essere verde ora…

Che succede?
Ti ricordi che avevamo un test specifico per assicurare il light-dom di a11y-input?
Quindi anche se l’utente mette solo <a11y-input></a11y-input> nel suo codice – quello che effettivamente viene fuori è

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

per esempio a11y-input sta effettivamente creando nodi dentro il tuo my-app shadow dom. Assurdo! Per il nostro esempio qui diciamo che è quello che vogliamo.
Quindi come possiamo ancora testarlo?

Per fortuna .shadowDom ha un altro asso nella manica; ci permette di ignorare parti di dom.

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

Possiamo anche specificare le seguenti proprietà:

  • ignoreChildren
  • ignoreTags
  • ignoreAttributes (globalmente o per tag specifici)

Per maggiori dettagli vedi semantic-dom-diff.

Test delle istantanee

Se hai molti alberi di dom grandi, scrivere/mantenere tutte quelle aspettative scritte manualmente sarà difficile.
Per aiutarti in questo ci sono le istantanee semi/automatiche.

Così se cambiamo il nostro codice

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

Se ora eseguiamo npm run test creerà un file __snapshots__/a11y input.md e lo riempirà con qualcosa come questo

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

Quello che abbiamo scritto a mano prima può ora essere generato automaticamente all’avvio o forzatamente tramite npm run test:update-snapshots.

Se il file __snapshots__/a11y input.md esiste già, lo confronterà con l’output e si avranno errori se l’output html è cambiato.

Per maggiori dettagli vedi semantic-dom-diff.

Penso che ora sia abbastanza sul confronto degli alberi dom…
È ora di cambiare 🤗

Copertura del codice

Un’altra metrica utile che otteniamo quando testiamo con la configurazione open-wc è la copertura del codice.
Cosa significa e come possiamo ottenerla? La copertura del codice è una misura di quanto del nostro codice è controllato dai test. Se c’è una linea, una dichiarazione, una funzione o un ramo (ad esempio if/else dichiarazione) che i nostri test non coprono il nostro punteggio di copertura sarà influenzato.
Un semplice npm run test è tutto ciò di cui abbiamo bisogno e si otterrà quanto segue:

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

Il che significa che il 100% delle dichiarazioni, rami, funzioni e linee del nostro codice sono coperte dai test. Piuttosto pulito!

Perciò facciamo il contrario e aggiungiamo codice a src/a11y-input.js prima di aggiungere un test. Diciamo che vogliamo accedere al valore del nostro input direttamente tramite il nostro elemento personalizzato e ogni volta che il suo valore è ‘cat’ vogliamo registrare qualcosa.

È un risultato molto diverso

La nostra copertura è molto più bassa di prima. Il nostro comando di test addirittura fallisce, anche se tutti i test sono stati eseguiti con successo.
Questo perché per default la configurazione di open-wc imposta una soglia del 90% per la copertura del codice.

Se vogliamo migliorare la copertura dobbiamo aggiungere dei test – quindi facciamolo

uh oh 😱 volevamo migliorare la copertura ma ora dobbiamo prima risolvere un bug reale 😞

Questo è stato inaspettato… a prima vista, non so davvero cosa significhi… Meglio controllare alcuni nodi reali e ispezionarli nel browser.

Debugging nel browser

Quando eseguiamo il nostro test con watch, karma imposta un ambiente browser persistente in cui eseguire i test.

  • Assicurati di aver iniziato con npm run test:watch
  • visita http://localhost:9876/debug.html

Dovresti vedere qualcosa del genere

Puoi cliccare sul pulsante play cerchiato per eseguire solo un singolo test.

Apriamo il Chrome Dev Tools (F12) e mettiamo un debugger nel codice del test.

Dang… l’errore avviene anche prima di quel punto…
Errori “fatali” come questo sono un po’ più difficili perché non sono test falliti ma una sorta di fusione completa del tuo componente completo.

Ok, mettiamo del codice nel setter direttamente.

set value(newValue) { debugger;

Va bene, questo ha funzionato, quindi nella nostra console di chrome scriviamo console.log(this)vediamo cosa abbiamo qui

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

Ecco qui – la dom shadow non è ancora resa quando il setter viene chiamato.
Quindi andiamo sul sicuro e aggiungiamo un controllo prima

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

L’errore di Fatel è sparito 🎉
Ma ora abbiamo un test non riuscito 😭

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

Potremmo aver bisogno di un cambio di tattica 🤔
Possiamo aggiungerlo come una proprietà value separata e sincronizzare quando necessario.

E finalmente siamo di nuovo in affari! 🎉

ok bug risolto – possiamo per favore tornare alla copertura? grazie 🙏

Ritorno alla copertura

Con questo test aggiunto abbiamo fatto dei progressi.

Tuttavia non siamo ancora del tutto a posto – la domanda è perché?

Per scoprirlo apri coverage/index.html nel tuo browser. Non c’è bisogno di un server web, basta aprire il file nel tuo browser – su un mac puoi farlo dalla linea di comando con open coverage/index.html

Vedrai qualcosa del genere

Una volta che clicchi su a11y-input.js ottieni un’informazione riga per riga su quanto spesso sono state eseguite.
Così possiamo vedere immediatamente quali linee non sono ancora eseguite dai nostri test.

Aggiungiamo quindi un test per questo

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

Con questo, siamo tornati al 100% sulle dichiarazioni ma ci manca ancora qualcosa sui rami.
Vediamo perché?

Questo E significa else path not taken.
Quindi ogni volta che la funzione update viene chiamata c’è sempre una proprietà value nella changedProperties.

Abbiamo anche label quindi è una buona idea testarla. 👍

boom 100% 💪 abbiamo vinto 🥇

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

Ma aspetta non abbiamo nemmeno finito il test di cui sopra – il codice è ancora

 // somehow check that console.log was called

Come mai abbiamo il 100% di copertura dei test?

Prima cerchiamo di capire come funziona la copertura del codice 🤔
Il modo in cui la copertura del codice viene misurata è applicando una forma di instrumentation. In breve, prima che il nostro codice venga eseguito viene modificato (instrumented) e si comporta in questo modo:

Nota: Questa è una versione super semplificata a scopo illustrativo.

In pratica, il tuo codice viene disseminato di molti molti flag. In base a quali flag vengono attivati viene creata una statistica.

Così il 100% di copertura dei test significa solo che ogni linea che avete nel vostro codice è stata eseguita almeno una volta dopo che tutti i vostri test sono finiti. Non significa che avete testato tutto, o se i vostri test fanno le asserzioni corrette.

Quindi anche se abbiamo già il 100% di copertura del codice dobbiamo ancora migliorare il nostro test di log.

Dovreste, quindi, vedere la copertura del codice come uno strumento che vi dà solo una guida e un aiuto per individuare alcuni test mancanti, piuttosto che una garanzia assoluta della qualità del codice.

Spiare il codice

Se volete controllare quanto spesso o con quali parametri una funzione viene chiamata, questo si chiama spiare.
open-wc raccomanda il venerabile pacchetto sinon, che fornisce molti strumenti per lo spionaggio e altri compiti correlati.

npm i -D sinon

Così si crea una spia su un oggetto specifico e poi si può controllare quanto spesso viene chiamato.

Uh oh… il test fallisce:

AssertionError: expected 0 to equal 1

Mischiare con oggetti globali come console potrebbe avere effetti collaterali quindi è meglio rifattorizzare usando una funzione di log dedicata.

Questo risulta in nessun oggetto globale nel nostro codice di test – dolce 🤗

Tuttavia, abbiamo ancora lo stesso errore. Facciamo il debug… boohoo apparentemente update non è sync – una supposizione sbagliata che ho fatto 🙈 Sto dicendo che le supposizioni sono pericolose abbastanza spesso – ancora ci casco di tanto in tanto 😢.

Quindi cosa possiamo fare? Purtroppo non sembra esserci un’api pubblica per fare alcune azioni di sincronizzazione innescate da un aggiornamento delle proprietà.
Creiamo un problema per questo https://github.com/Polymer/lit-element/issues/643.

Per ora apparentemente, l’unico modo è affidarsi ad un’api privata. 🙈
Inoltre, avevamo bisogno di spostare la sincronizzazione dei valori a updated in modo che venisse eseguita dopo ogni rendering della dom.

ed ecco il test aggiornato per il logging

wow, è stato un po’ più difficile del previsto ma ce l’abbiamo fatta 💪

SUMMARY:✔ 7 tests completedTOTAL: 7 SUCCESS

Esecuzione di test senza Karma Framework

Il framework Karma è potente e ricco di funzionalità, ma a volte potremmo voler ridurre il nostro regime di test. La cosa bella di tutto ciò che abbiamo proposto finora è che abbiamo usato solo moduli es standard del browser senza bisogno di transpilazione, con l’unica eccezione degli specificatori di moduli nudi.
Quindi basta creare un test/index.html.

e aprendolo tramite owc-dev-server in chrome, funzionerà perfettamente.
Abbiamo tutto pronto e funzionante senza webpack o karma – dolce 🤗

Do the Cross-Browser Thing

Ora ci sentiamo abbastanza a nostro agio con il nostro componente web. È testato e coperto; c’è solo un altro passo – vogliamo essere sicuri che funzioni e sia testato in tutti i browser.

Quindi eseguiamolo

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

Sì, funziona bene! 🤗

Se ci sono test che falliscono, li mostrerà nel sommario con il browser specifico dove è fallito.

Se hai bisogno di fare il debug di un browser particolare:

Anche se vuoi regolare il browser che viene testato puoi regolare il tuo karma.bs.config.js.

Per esempio, se vuoi aggiungere il Firefox ESR alla tua lista.

O forse vuoi testare solo 2 browser specifici?

Nota: Questo usa le strategie di webpack merge replace.

Ricordo veloce

  • I test sono importanti per ogni progetto. Assicuratevi di scriverne il più possibile.
  • Cercate di mantenere alta la copertura del codice, ma ricordate che non è una garanzia magica, quindi non ha sempre bisogno di essere al 100%.
  • Debug nel browser tramite npm run test:watch. Per i browser legacy, usa npm run test:legacy.watch.

Cosa c’è dopo?

  • Esegui i test nella tua CI (funziona perfettamente insieme a browserstack). Vedi i nostri consigli su automatizzare.

Seguiteci su Twitter, o seguitemi sul mio Twitter personale.
Assicurati di controllare gli altri strumenti e le raccomandazioni su open-wc.org.

Grazie a Pascal e Benny per il feedback e per avermi aiutato a trasformare i miei scarabocchi in una storia seguibile.