Articles

Flux de travail de test pour les composants Web

Quand vous expédiez quelque chose qui sera utilisé par d’autres, vous prenez la responsabilité de livrer un code sûr et stable. Une façon d’aborder cela est de tester votre code.

Peu importe la taille – peu importe la simplicité de votre projet, il devrait toujours idéalement y avoir des tests.

Oui, je sais que la réalité frappe fort et il y aura de nombreux cas où cela n’arrive pas – mais vous devriez toujours vous efforcer d’avoir des tests

Disclaimer

Dans ce tutoriel, nous allons faire une version simple d’un élément d’entrée. À la fin de celui-ci, vous allez acquérir les compétences et les connaissances pour mettre en pratique les outils de test open-wc ; et construire un composant d’entrée solide, accessible et bien testé.

Avertissement

C’est un tutoriel approfondi montrant quelques pièges et cas difficiles lors du travail avec les composants web. C’est définitivement pour les utilisateurs plus avancés. Vous devez avoir une connaissance de base de LitElement et des types JSDoc. Avoir une idée de ce qu’est Mocha, Chai BDD, Karma pourrait aider un peu aussi.

Nous pensons à poster une version plus digeste de ceci donc si c’est quelque chose que vous aimeriez voir – faites-le nous savoir dans les commentaires.

Si vous voulez jouer le long – tout le code est sur github.

Démarrons !

Lancer dans votre 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

Pour plus de détails, veuillez voir https://open-wc.org/testing/.

Supprimer src/A11yInput.js

Modifier src/a11y-input.js en:

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

Nos tests jusqu’à présent consistent en une seule fonctionnalité (la propriété label) et une seule assertion (expect). Nous utilisons la syntaxe BDD de karma et chai, donc nous regroupons les ensembles de tests (it) sous les fonctionnalités ou API auxquelles ils se rapportent (describe).

Voyons si tout fonctionne correctement en exécutant : npm run test.

Awesome – comme prévu (🥁), nous avons un test défaillant 🙂

Passons en mode veille, qui exécutera les tests en continu dès que vous apporterez des modifications à votre code.

npm run test:watch

Le code suivant a été ajouté dans la vidéo ci-dessus à src/a11y-input.js:

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

Alors, tout va bien ? Vous êtes toujours avec nous ? Super ! Relevons un peu le jeu…

Ajout d’un test pour le shadow DOM

Ajoutons une assertion pour tester le contenu de la racine shadow de notre élément.

Si nous voulons être sûrs que notre élément se comporte/apparaît de la même manière, nous devons nous assurer que sa structure dom reste la même.
Considérons donc le shadow dom actuel par rapport à ce que nous voulons qu’il soit.

Comme prévu, nous obtenons :

Donc implémentons cela dans notre élément.

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

Intéressant, le test devrait être vert… mais il ne l’est pas 🤔 Jetons un coup d’œil.

Vous avez peut-être remarqué ces étranges balises vides de commentaire <!---->. Ce sont des marqueurs que lit-html utilise pour se souvenir de l’emplacement des parties dynamiques, afin qu’il puisse être mis à jour efficacement. Pour les tests, cependant, cela peut être un peu ennuyeux à gérer.

Si nous utilisons innerHTML pour comparer le DOM, nous devrions compter sur une simple égalité des chaînes de caractères. Dans ces circonstances, nous devrions faire correspondre exactement l’espace blanc, les commentaires, etc. du DOM généré ; en d’autres termes : il devra s’agir d’une correspondance parfaite. En fait, tout ce que nous devons tester, c’est que les éléments que nous voulons rendre sont rendus. Nous voulons tester le contenu sémantique de la racine shadow.

Donc, essayons-le 💪

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

Comment fonctionne shadowDom.to.equal() ?

  1. Il récupère le innerHTML de la racine shadow
  2. Il l’analyse (en fait, le navigateur l’analyse – aucune bibliothèque n’est nécessaire)
  3. Normalise-la (potentiellement chaque balise/propriété sur sa propre ligne)
  4. Analyse et normalise la chaîne HTML attendue
  5. Passe les deux chaînes DOM normalisées à la fonction de comparaison par défaut de chai
  6. En cas d’échec, regroupe, et affiche toutes les différences de manière claire

Si vous voulez en savoir plus, consultez la documentation de semantic-dom-diff.

Tester le DOM « léger »

Nous pouvons faire exactement la même chose avec le DOM léger. (Le DOM qui sera fourni par notre utilisateur ou nos valeurs par défaut, c’est-à-dire le children de l’élément).

Et mettons-le en œuvre.

Nous avons donc testé notre dom d’ombre et de lumière 💪 et nos tests se déroulent proprement 🎉

Note : L’utilisation de l’API DOM dans le cycle de vie d’un lit-élément est un anti-modèle, cependant pour permettre l’a11y, cela pourrait être un cas d’utilisation réel – de toute façon, c’est génial à des fins d’illustration

Utilisation de notre élément dans une application

Alors maintenant que nous avons une entrée a11y de base, utilisons-la – et testons-la – dans notre application.

De nouveau nous commençons avec un squelette src/my-app.js

Et notre test dans test/my-app.test.js;

Run the test => échoue et ensuite nous ajoutons l’implémentation à src/a11y-input.js

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

Mais oh non ! Cela devrait être vert maintenant…

Que se passe-t-il ?
Vous vous souvenez que nous avions un test spécifique pour assurer le light-dom de a11y-input ?
Donc, même si les utilisateurs ne mettent que <a11y-input></a11y-input> dans son code – ce qui sort réellement est

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

par exemple a11y-input est en train de créer des nœuds à l’intérieur de votre shadow dom my-app. C’est absurde ! Pour notre exemple ici, nous disons que c’est ce que nous voulons.
Alors, comment pouvons-nous encore le tester ?

Heureusement, .shadowDom a un autre as dans sa manche ; il nous permet d’ignorer des parties de dom.

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

Nous pouvons même spécifier les propriétés suivantes également:

  • ignoreChildren
  • ignoreTags
  • ignoreAttributes (globalement ou pour des balises spécifiques)

Pour plus de détails, veuillez consulter semantic-dom-diff.

Tests d’instantanés

Si vous avez beaucoup de grands arbres de dom écrire/maintenir tous ces expects écrits manuellement va être difficile.
Pour vous aider avec cela il y a des instantanés semi-automatiques.

Si nous changeons notre code

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

Si nous exécutons maintenant npm run test, il créera un fichier __snapshots__/a11y input.md et le remplira avec quelque chose comme ceci

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

Ce que nous avons écrit avant à la main peut maintenant être auto-généré sur init ou de manière forcée via npm run test:update-snapshots.

Si le fichier __snapshots__/a11y input.md existe déjà, il le comparera avec la sortie et vous obtiendrez des erreurs si votre sortie html a changé.

Pour plus de détails, veuillez voir semantic-dom-diff.

Je pense que c’est maintenant assez sur la comparaison des arbres de dom…
Il est temps de changer 🤗

Couverture de code

Une autre métrique utile que nous obtenons lorsque nous testons avec la configuration open-wc est la couverture de code.
Alors qu’est-ce que cela signifie et comment pouvons-nous l’obtenir ? La couverture du code est une mesure de la quantité de notre code qui est vérifiée par les tests. S’il y a une ligne, une déclaration, une fonction ou une branche (par exemple, la déclaration if/else) que nos tests ne couvrent pas, notre score de couverture sera affecté.
Un simple npm run test est tout ce dont nous avons besoin et vous obtiendrez ce qui suit :

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

Ce qui signifie que 100% des déclarations, branches, fonctions et lignes de notre code sont couvertes par les tests. Plutôt chouette !

Alors, allons dans l’autre sens et ajoutons du code à src/a11y-input.js avant d’ajouter un test. Disons que nous voulons accéder à la valeur de notre entrée directement via notre élément personnalisé et chaque fois que sa valeur est ‘cat’, nous voulons enregistrer quelque chose.

C’est un résultat largement différent

Notre couverture est bien plus faible qu’avant. Notre commande de test échoue même, alors que tous les tests s’exécutent avec succès.
C’est parce que par défaut la configuration d’open-wc fixe un seuil de 90% pour la couverture du code.

Si nous voulons améliorer la couverture, nous devons ajouter des tests – alors faisons-le

uh oh 😱 nous voulions améliorer la couverture mais maintenant nous devons d’abord corriger un bug réel 😞

C’était inattendu… à première vue, je ne sais pas vraiment ce que cela signifie…. mieux vaut vérifier certains nœuds réels et les inspecter dans le navigateur.

Débogage dans le navigateur

Lorsque nous exécutons notre test avec watch, karma met en place un environnement de navigateur persistant pour exécuter les tests.

  • Soyez sûr d’avoir commencé avec npm run test:watch
  • visiter http://localhost:9876/debug.html

Vous devriez voir quelque chose comme ceci

Vous pouvez cliquer sur le bouton de lecture encerclé pour ne lancer qu’un seul test individuel.

Ouvrons donc les outils de développement de Chrome (F12) et mettons un débogueur dans le code du test.

Dang… l’erreur se produit même avant ce point…
Les erreurs « fatales » comme celle-ci sont un peu plus difficiles car elles ne sont pas des tests défaillants mais une sorte de fusion complète de votre composant complet.

Ok, mettons du code dans le setter directement.

set value(newValue) { debugger;

Alright, cela a fonctionné donc notre console chrome nous écrivons console.log(this) voyons ce que nous avons ici

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

Ahh là nous l’avons – le shadow dom n’est pas encore rendu lorsque le setter est appelé.
Saluons donc la sécurité et ajoutons une vérification avant

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

L’erreur Fatel est partie 🎉
Mais nous avons maintenant un test qui échoue 😭

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

Nous pouvons avoir besoin d’un changement de tactique 🤔
Nous pouvons l’ajouter comme une propriété value séparée et synchroniser quand c’est nécessaire.

Et nous sommes enfin de retour aux affaires ! 🎉

ok bug corrigé – pouvons-nous s’il vous plaît revenir à la couverture ? Merci 🙏

Retour à la couverture

Avec ce test ajouté, nous avons fait quelques progrès.

Mais nous ne sommes toujours pas complètement là – la question est pourquoi ?

Pour le découvrir, ouvrez coverage/index.html dans votre navigateur. Pas besoin de serveur web, il suffit d’ouvrir le fichier dans votre navigateur – sur un mac, vous pouvez le faire à partir de la ligne de commande avec open coverage/index.html

Vous verrez quelque chose comme ceci

Une fois que vous cliquez sur a11y-input.js vous obtenez une info ligne par ligne combien de fois elles ont été exécutées.
Donc nous pouvons immédiatement voir quelles lignes ne sont pas encore exécutées par nos tests.

Alors ajoutons un test pour cela

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

Avec cela, nous sommes de retour à 100% sur les déclarations mais il nous manque encore quelque chose sur les branches.
Voyons pourquoi ?

Ce E signifie else path not taken.
Donc, à chaque fois que la fonction update est appelée, il y a toujours une propriété value dans les changedProperties.

Nous avons aussi label, c’est donc une bonne idée de la tester. 👍

boom 100% 💪 on gagne 🥇

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

Mais attendez, nous n’avons même pas fini le test ci-dessus – le code est toujours

 // somehow check that console.log was called

Comment se fait-il que nous ayons une couverture de test de 100% ?

Essayons d’abord de comprendre comment fonctionne la couverture du code 🤔
La façon dont la couverture du code est mesurée est en appliquant une forme de instrumentation. En bref, avant que notre code ne soit exécuté, il est modifié (instrumented) et il se comporte comme suit:

Note : Il s’agit d’une version super simplifiée à des fins d’illustration.

Basiquement, votre code devient jonché de beaucoup beaucoup de drapeaux. En fonction des drapeaux qui sont déclenchés, une statistique est créée.

Donc une couverture de test de 100% signifie seulement que chaque ligne que vous avez dans votre code a été exécutée au moins une fois après la fin de tous vos tests. Cela ne signifie pas que vous avez tout testé, ou que vos tests font les assertions correctes.

Donc, même si nous avons déjà une couverture de code de 100%, nous allons encore améliorer notre test de journal.

Vous devriez, par conséquent, voir la couverture du code comme un outil qui vous donne seulement des conseils et de l’aide pour repérer certains tests manquants, plutôt qu’une garantie dure de la qualité du code.

Espionnage du code

Si vous voulez vérifier à quelle fréquence ou avec quels paramètres une fonction est appelée, cela s’appelle de l’espionnage.
open-wc recommande le vénérable paquet sinon, qui fournit de nombreux outils pour l’espionnage et d’autres tâches connexes.

npm i -D sinon

Donc vous créez un espion sur un objet spécifique et ensuite vous pouvez vérifier combien de fois il est appelé.

Uh oh…. le test échoue:

AssertionError: expected 0 to equal 1

Messing with global objects like console might have side effects so let’s better refactor using a dedicated log function.

This result in no global object in our test code – sweet 🤗

However, we still get the same error. Déboguons… boohoo apparemment update n’est pas synchrone – une mauvaise hypothèse que j’ai faite 🙈 Je dis que les hypothèses sont dangereuses assez souvent – encore je tombe pour elle de temps en temps 😢.

Alors que pouvons-nous faire ? Malheureusement, il semble qu’il n’y ait pas d’api publique pour faire certaines actions de synchronisation déclenchées par une mise à jour de propriété.
Créons une question pour cela https://github.com/Polymer/lit-element/issues/643.

Pour l’instant apparemment, le seul moyen est de s’appuyer sur une api privée. 🙈
De plus, nous devions déplacer la synchronisation des valeurs à updatedafin qu’elle soit exécutée après chaque rendu de dom.

et voici le test mis à jour pour la journalisation

wow, c’était un peu plus difficile que prévu mais nous l’avons fait 💪

SUMMARY:✔ 7 tests completedTOTAL: 7 SUCCESS

Exécuter des tests sans le framework Karma

Le framework Karma est puissant et riche en fonctionnalités, mais parfois nous pourrions vouloir réduire notre régiment de tests. Ce qui est bien avec tout ce que nous avons proposé jusqu’à présent, c’est que nous n’avons utilisé que des modules es standard du navigateur, sans besoin de transpilation, à la seule exception des spécificateurs de modules nus.
Donc, juste en créant un test/index.html.

et en l’ouvrant via owc-dev-server dans chrome, cela fonctionnera parfaitement.
Nous avons tout mis en place et en marche sans webpack ou karma – doux 🤗

Faire la chose cross-browser

Nous sommes maintenant assez à l’aise avec notre composant web. Il est testé et couvert ; il ne reste plus qu’une étape – nous voulons nous assurer qu’il fonctionne et est testé dans tous les navigateurs.

Alors, exécutons-le

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

Ouais, ça marche bien ! 🤗

S’il y a des tests qui échouent, il les sortira dans le résumé avec le navigateur spécifique où il a échoué.

Si vous avez besoin de déboguer un navigateur particulier:

Aussi si vous voulez ajuster le navigateur qui est testé, vous pouvez ajuster votre karma.bs.config.js.

Par exemple, si vous voulez ajouter le Firefox ESR à votre liste.

Ou peut-être que vous voulez tester seulement 2 navigateurs spécifiques ?

Note : Ceci utilise les stratégies de fusion de webpack replace.

Récapitulation rapide

  • Les tests sont importants pour chaque projet. Assurez-vous d’en écrire autant que vous le pouvez.
  • Essayez de garder votre couverture de code élevée, mais rappelez-vous que ce n’est pas une garantie magique, donc il n’est pas toujours nécessaire d’être à 100%.
  • Déboguez dans le navigateur via npm run test:watch. Pour les anciens navigateurs, utilisez npm run test:legacy.watch.

Qu’est-ce qu’il y a ensuite ?

  • Exécutez les tests dans votre CI (fonctionne parfaitement bien avec browserstack). Voir nos recommandations à l’automatisation.

Suivez-nous sur Twitter, ou suivez-moi sur mon Twitter personnel.
Ne manquez pas de consulter nos autres outils et recommandations sur open-wc.org.

Merci à Pascal et Benny pour leurs commentaires et pour avoir aidé à transformer mes gribouillages en une histoire qui se suit.