Articles

Pracovní postup testování webových komponent

Kdykoli dodáváte něco, co mají používat ostatní, přebíráte odpovědnost za dodání bezpečného a stabilního kódu. Jedním ze způsobů, jak to řešit, je testování kódu.

Nezáleží na tom, jak malý – jak jednoduchý je váš projekt, vždy by v ideálním případě měly existovat testy.

Ano, vím, že realita tvrdě naráží a bude mnoho případů, kdy se tak nestane – ale vždy byste se měli snažit mít testy

Disclaimer

V tomto tutoriálu vytvoříme jednoduchou verzi vstupního prvku. Na jeho konci získáte dovednosti a znalosti, které vám umožní využít testovací nástroje open-wc v praxi; a vytvořit solidní, dostupnou a dobře otestovanou vstupní komponentu.

Upozornění

Jedná se o podrobný tutoriál, který ukazuje několik úskalí a těžkých případů při práci s webovými komponentami. Rozhodně je určen pro pokročilejší uživatele. Měli byste mít základní znalosti o typech LitElement a JSDoc. Mít představu o tom, co je Mocha, Chai BDD, Karma, by mohlo také trochu pomoci.

Přemýšlíme o zveřejnění snadněji stravitelné verze tohoto návodu, takže pokud je to něco, co byste rádi viděli – dejte nám vědět v komentářích.

Pokud si chcete hrát spolu – veškerý kód je na githubu.

Začněme!

Spustit v konzoli

$ 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

Další podrobnosti najdete v https://open-wc.org/testing/.

Odstraňte src/A11yInput.js

Změňte src/a11y-input.js na:

a test/a11y-input.test.js na:

Naše testy se zatím skládají z jediné funkce (vlastnost label) a jediného tvrzení (expect). Používáme syntaxi BDD karma a chai, takže seskupujeme sady testů (it) pod vlastnosti nebo API, kterých se týkají (describe).

Podíváme se, zda vše funguje správně spuštěním: npm run test.

Paráda – přesně podle očekávání (🥁) máme neúspěšný test 🙂

Přepneme se do režimu sledování, který bude testy spouštět průběžně, kdykoli provedete změny v kódu.

npm run test:watch

Následující kód byl přidán ve výše uvedeném videu do src/a11y-input.js:

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

Tak co, zatím dobrý? Jste stále s námi? Skvělé! Pojďme trochu přidat…

Přidání testu pro stínový DOM

Přidáme tvrzení pro testování obsahu stínového kořene našeho elementu.

Chceme-li mít jistotu, že se náš element chová/vypadá stejně, měli bychom se ujistit, že jeho struktura dom zůstává stejná.
Porovnejme tedy skutečný stínový dom s tím, jaký chceme, aby byl.

Podle očekávání dostaneme:

Takže to implementujme do našeho elementu.

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

Zajímavé, test by měl být zelený… ale není 🤔 Podívejme se na to.

Možná jste si všimli těch podivných prázdných značek komentáře <!---->. Jsou to značky, které lit-html používá, aby si pamatoval, kde se nacházejí dynamické části, a mohl je tak efektivně aktualizovat. Pro testování to však může být trochu otravné.

Pokud bychom k porovnání DOM použili innerHTML, museli bychom se spolehnout na prostou rovnost řetězců. Za těchto okolností bychom se museli přesně shodovat s bílými znaky, komentáři atd. ve vygenerovaném DOM; jinými slovy: bude muset jít o dokonalou shodu. Ve skutečnosti potřebujeme pouze otestovat, zda jsou prvky, které chceme vykreslit, vykresleny. Chceme otestovat sémantický obsah kořene stínu.

Takže to vyzkoušíme 💪

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

Jak funguje funkce shadowDom.to.equal()?

  1. Získá innerHTML kořen stínu
  2. Přečte ho (vlastně, prohlížeč rozebere – není potřeba žádná knihovna)
  3. Normalizuje jej (potenciálně každý tag/vlastnost na vlastním řádku)
  4. Rozdělí a normalizuje očekávaný řetězec HTML
  5. Předá oba normalizované řetězce DOM výchozí porovnávací funkci chai
  6. V případě selhání, seskupí a přehledně zobrazí případné rozdíly

Pokud chcete vědět více, podívejte se do dokumentace funkce semantic-dom-diff.

Testování „lehkého“ DOM

Přesně totéž můžeme provést s lehkým DOM. (DOM, který nám poskytne uživatel nebo naše výchozí nastavení, tedy children prvku).

A nyní to implementujme.

Takže jsme otestovali náš světelný a stínový dom 💪 a naše testy proběhly čistě 🎉

Pozn: Použití DOM API v životním cyklu světelného prvku je anti-vzor, nicméně pro umožnění a11y to může být reálný případ použití – každopádně je to skvělé pro ilustrační účely

Použití našeho prvku v aplikaci

Takže nyní, když máme základní vstup a11y, použijme jej – a otestujme – v naší aplikaci.

Znovu začneme s kostrou src/my-app.js

A náš test v test/my-app.test.js;

Spuštění testu => selže a pak přidáme implementaci do src/a11y-input.js

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

Ale ne! To už by mělo být zelené…

Co se děje?
Pamatujete si, že jsme měli zvláštní test, který měl zajistit světelný dom a11y-input?“
Takže i když uživatelé do svého kódu vloží pouze <a11y-input></a11y-input> – to, co ve skutečnosti vyjde, je

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

např. a11y-input ve skutečnosti vytváří uzly uvnitř vašeho stínového dom my-app. Absurdní! Pro náš příklad zde říkáme, že to je to, co chceme.
Jak to tedy ještě můžeme otestovat?

Naštěstí má .shadowDom další eso v rukávu; umožňuje nám ignorovat části domů.

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

Můžeme dokonce specifikovat i následující vlastnosti:

  • ignoreChildren
  • ignoreTags
  • ignoreAttributes (globálně nebo pro konkrétní značky)

Podrobněji viz sémantický-dom-diff.

Testování snímků

Pokud máte hodně velkých domových stromů, bude psaní/udržování všech těch ručně psaných očekávání náročné.
Pomoci vám s tím mohou poloautomatické snímky.

Změníme-li tedy náš kód

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

Pokud nyní spustíme npm run test, vytvoří se soubor __snapshots__/a11y input.md a naplní se něčím takovým

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

To, co jsme předtím napsali ručně, se nyní může automaticky generovat při initu nebo násilně pomocí npm run test:update-snapshots.

Pokud soubor __snapshots__/a11y input.md již existuje, porovná ho s výstupem a pokud se html výstup změnil, zobrazí se chyby.

Další podrobnosti najdete v semantic-dom-diff.

Myslím, že teď už bylo dost o porovnávání stromů domů…
Je čas na změnu 🤗

Code coverage

Další užitečnou metrikou, kterou získáme při testování s nastavením open-wc, je code coverage.
Co to tedy znamená a jak to můžeme získat? Pokrytí kódu je měřítkem toho, jak velká část našeho kódu je kontrolována testy. Pokud existuje řádek, příkaz, funkce nebo větev (např. příkaz if/else), které naše testy nepokrývají, ovlivní to naše skóre pokrytí.
Stačí nám jednoduchý npm run test a dostaneme následující:

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

To znamená, že 100 % příkazů, větví, funkcí a řádků našeho kódu je pokryto testy. Pěkné!“

Půjdeme tedy opačným směrem a přidáme kód do src/a11y-input.js před přidáním testu. Řekněme, že chceme přistupovat k hodnotě našeho vstupu přímo prostřednictvím našeho vlastního prvku a kdykoli je jeho hodnota ‚cat‘, chceme něco zaznamenat.

Je to úplně jiný výsledek

Naše pokrytí je mnohem nižší než předtím. Náš testovací příkaz dokonce selže, přestože všechny testy proběhly úspěšně.
Je to proto, že ve výchozím nastavení konfigurace open-wc je nastavena 90% hranice pro pokrytí kódu.

Pokud chceme zlepšit pokrytí, musíme přidat testy – tak to uděláme

uh aha 😱 chtěli jsme zlepšit pokrytí, ale teď musíme nejdřív opravit aktuální chybu 😞

To bylo nečekané… na první pohled opravdu nevím, co to znamená… lepší je zkontrolovat nějaké skutečné uzly a prohlédnout si je v prohlížeči.

Ladění v prohlížeči

Když spustíme náš test pomocí watch, karma nastaví trvalé prostředí prohlížeče, ve kterém se testy spustí.

  • Ujistěte se, že jste začali npm run test:watch
  • navštívit http://localhost:9876/debug.html

Měli byste vidět něco takového

Můžete kliknout na zakroužkované tlačítko přehrát a spustit pouze jeden jednotlivý test.

Otevřeme tedy Chrome Dev Tools (F12) a vložíme debugger do kódu testu.

Propána… chyba nastane ještě před tímto bodem…
„Fatální“ chyby, jako je tato, jsou trochu těžší, protože se nejedná o selhání testů, ale o jakési kompletní zhroucení celé vaší komponenty.

Ok, vložíme nějaký kód přímo do setter.

set value(newValue) { debugger;

Dobře, to se povedlo, takže do naší konzole chrome napíšeme console.log(this) podívejme se, co tu máme

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

Aha, máme to – stínový dom ještě není vykreslen, když je zavolán setter.
Takže pro jistotu přidáme kontrolu před

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

Chybka Fatel je pryč 🎉
Ale teď máme neúspěšný test 😭

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

Možná bude potřeba změnit taktiku 🤔
Můžeme ji přidat jako samostatnou vlastnost value a synchronizovat podle potřeby.

A konečně jsme zase v provozu! 🎉

ok chyba opravena – můžeme se prosím vrátit k pokrytí? děkuji 🙏

Zpět k pokrytí

Díky tomuto přidanému testu jsme dosáhli určitého pokroku.

Stále však nejsme zcela u cíle – otázkou je proč?

Pro zjištění otevřete coverage/index.html v prohlížeči. Není potřeba žádný webový server, stačí soubor otevřít v prohlížeči – na Macu to můžete udělat z příkazového řádku pomocí open coverage/index.html

Uvidíte něco takového

Po kliknutí na a11y-input.js se vám zobrazí informace o tom, jak často se řádek po řádku provedl.
Takže hned vidíme, které řádky naše testy ještě neprovedly.

Takže přidáme test na to

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

Tímto jsme opět na 100% u příkazů, ale stále nám něco chybí u větví.
Podíváme se proč?

To E znamená else path not taken.
Takže kdykoliv se zavolá funkce update, vždy je v changedProperties vlastnost value.

Máme i label, takže je dobré to otestovat. 👍

bum 100% 💪 máme vyhráno 🥇

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

Ale počkat, ani jsme nedokončili výše uvedený test – kód je stále

 // somehow check that console.log was called

Jak to, že máme 100% pokrytí testy?

Nejprve se pokusíme pochopit, jak pokrytí kódu funguje 🤔
Způsob, jakým se pokrytí kódu měří, je použití formy instrumentation. Stručně řečeno, než se náš kód spustí, změní se (instrumented) a chová se nějak takto:

Poznámka: Toto je super zjednodušená verze pro ilustrační účely.

Zásadně se váš kód zahltí mnoha mnoha příznaky. Na základě toho, které příznaky se spustí, se vytvoří statistika.

Takže 100% pokrytí testy znamená pouze to, že každý řádek, který máte v kódu, byl proveden alespoň jednou po dokončení všech vašich testů. Neznamená to, že jste otestovali všechno, nebo jestli vaše testy provádějí správná tvrzení.

Takže i když už máme 100% pokrytí kódu, budeme ještě vylepšovat náš test logů.

Pokrytí kódu byste proto měli vnímat spíše jako nástroj, který vám pouze poradí a pomůže odhalit některé chybějící testy, než jako tvrdou záruku kvality kódu.

Špehování kódu

Chcete-li zkontrolovat, jak často nebo s jakými parametry se funkce volá, říká se tomu špehování.
open-wc doporučuje úctyhodný balík sinon, který poskytuje mnoho nástrojů pro špehování a další související úlohy.

npm i -D sinon

Takže vytvoříte špeha na určitý objekt a pak můžete kontrolovat, jak často je volán.

Aha… test selhává:

AssertionError: expected 0 to equal 1

Hrátky s globálními objekty, jako je console, mohou mít vedlejší účinky, takže raději refaktorujme pomocí speciální funkce log.

Výsledkem je, že v našem testovacím kódu není žádný globální objekt – sladké 🤗

Stále však dostáváme stejnou chybu. Pojďme ladit… boohoo zřejmě update není synchronizace – chybný předpoklad, který jsem učinil 🙈 Říkám, že předpoklady jsou nebezpečné poměrně často – přesto na ně čas od času naletím 😢.

Takže co můžeme dělat? Bohužel se zdá, že neexistuje žádné veřejné api, které by provádělo nějaké synchronizační akce vyvolané aktualizací vlastnosti.
Založíme pro to issue https://github.com/Polymer/lit-element/issues/643.

Prozatím je zřejmě jedinou možností spoléhat se na soukromé api. 🙈
Také jsme potřebovali přesunout synchronizaci hodnot do updated, aby se provedla po každém vykreslení dom.

a tady je aktualizovaný test pro logování

wow, to bylo trochu těžší, než jsme čekali, ale zvládli jsme to 💪

SUMMARY:✔ 7 tests completedTOTAL: 7 SUCCESS

Spuštění testů bez frameworku Karma

Framework Karma je výkonný a funkčně bohatý, ale někdy bychom možná chtěli zredukovat náš testovací režim. Příjemné na všem, co jsme dosud navrhli, je to, že jsme použili pouze es moduly standardní pro prohlížeče bez nutnosti transpilace, s jedinou výjimkou specifikátorů holých modulů.
Takže stačí vytvořit test/index.html.

a jeho otevřením přes owc-dev-server v Chromu to bude fungovat naprosto bez problémů.
Všechno jsme zprovoznili bez webpack nebo karma – sladké 🤗

Do the Cross-Browser Thing

Nyní se s naší webovou komponentou cítíme docela dobře. Je otestovaná a pokrytá; zbývá už jen jeden krok – chceme se ujistit, že běží a je otestovaná ve všech prohlížečích.

Takže ji prostě spustíme

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

Jo, to funguje pěkně! 🤗

Pokud dojde k neúspěšným testům, vypíše je v souhrnu s konkrétním prohlížečem, kde selhal.

Pokud potřebujete odladit konkrétní prohlížeč:

Jestliže chcete upravit prohlížeč, který se testuje, můžete upravit svůj karma.bs.config.js.

Například pokud chcete do seznamu přidat Firefox ESR.

Nebo možná chcete testovat pouze 2 konkrétní prohlížeče?

Poznámka: Toto používá nahrazení strategií sloučení webpack.

Rychlé shrnutí

  • Testování je důležité pro každý projekt. Určitě jich napište co nejvíce.
  • Snažte se udržet vysoké pokrytí kódu, ale pamatujte, že to není magická záruka, takže nemusí být vždy 100%.
  • Debugujte v prohlížeči přes npm run test:watch. Pro starší prohlížeče použijte npm run test:legacy.watch.

Co dál?

  • Spustit testy v CI (funguje výborně společně s browserstackem). Podívejte se na naše doporučení při automatizaci.

Sledujte nás na Twitteru nebo mě na mém osobním Twitteru.
Nezapomeňte se podívat na naše další nástroje a doporučení na open-wc.org.

Děkuji Pascalovi a Bennymu za zpětnou vazbu a za pomoc s přeměnou mých čmáranic na sledovatelný příběh.