Articles

Testing Workflow for Web Components

Kiedy wysyłasz coś, co ma być używane przez innych, bierzesz na siebie odpowiedzialność za dostarczenie bezpiecznego i stabilnego kodu. Jednym ze sposobów na rozwiązanie tego problemu jest testowanie kodu.

Nieważne jak mały – nieważne jak prosty projekt, zawsze powinny być testy.

Tak, wiem, że rzeczywistość uderza mocno i będzie wiele przypadków, w których tak się nie stanie – ale zawsze powinieneś dążyć do posiadania testów

Disclaimer

W tym tutorialu, zrobimy prostą wersję elementu wejściowego. Pod jego koniec, zdobędziesz umiejętności i wiedzę, aby zastosować narzędzia testujące open-wc w praktyce; i zbudować solidny, dostępny i dobrze przetestowany komponent input.

Ostrzeżenie

To jest dogłębny tutorial pokazujący kilka pułapek i trudnych przypadków podczas pracy z komponentami webowymi. To jest zdecydowanie dla bardziej zaawansowanych użytkowników. Powinieneś mieć podstawową wiedzę na temat LitElement i JSDoc Types. Posiadanie pojęcia czym jest Mocha, Chai BDD, Karma może również trochę pomóc.

Myślimy o opublikowaniu łatwiejszej do przyswojenia wersji tego, więc jeśli jest to coś co chciałbyś zobaczyć – daj nam znać w komentarzach.

Jeśli chcesz się pobawić – cały kod jest na githubie.

Zacznijmy!

Uruchom w swojej konsoli

$ 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

Więcej szczegółów znajdziesz w https://open-wc.org/testing/.

Usuń src/A11yInput.js

Zmodyfikuj src/a11y-input.js na:

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

Nasze testy jak dotąd składają się z pojedynczej cechy (właściwość label) i pojedynczej asercji (expect). Używamy składni BDD Karmy i Chai, więc grupujemy zestawy testów (it) pod cechami lub API, do których się odnoszą (describe).

Zobaczmy, czy wszystko działa poprawnie, uruchamiając: npm run test.

Awesome – tak jak się spodziewaliśmy (🥁), mamy test failing 🙂

Przełączmy się w tryb watch, który będzie uruchamiał testy w sposób ciągły za każdym razem, gdy będziesz wprowadzał zmiany w swoim kodzie.

npm run test:watch

Następujący kod został dodany w powyższym filmie do src/a11y-input.js:

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

Jak do tej pory, tak dobrze? Nadal jesteś z nami? Świetnie! Podkręćmy trochę grę…

Dodanie testu dla Shadow DOM

Dodajmy asercję testującą zawartość shadow root naszego elementu.

Jeśli chcemy być pewni, że nasz element zachowuje się/wygląda tak samo, powinniśmy upewnić się, że jego struktura dom pozostaje taka sama.
Porównajmy więc rzeczywisty shadow dom z tym, jaki chcemy, aby był.

Zgodnie z oczekiwaniami, otrzymujemy:

Zaimplementujmy to więc w naszym elemencie.

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

Interesujące, test powinien być zielony… ale nie jest 🤔 Przyjrzyjmy się temu.

Mogłeś zauważyć te dziwne puste znaczniki komentarza <!---->. Są to znaczniki, których lit-html używa do zapamiętania, gdzie znajdują się dynamiczne części, dzięki czemu może być sprawnie aktualizowany. Do testowania, jednak, może to być trochę irytujące, aby sobie z tym poradzić.

Jeśli użyjemy innerHTML do porównania DOM, musielibyśmy polegać na prostej równości łańcuchów. W tych okolicznościach, musielibyśmy dokładnie dopasować wygenerowany DOM do białych przestrzeni, komentarzy, itp. Naprawdę wszystko, co musimy przetestować, to fakt, że elementy, które chcemy renderować, są renderowane. Chcemy przetestować semantyczną zawartość shadow root.

Więc wypróbujmy to 💪

// 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 działa shadowDom.to.equal()?

  1. Pobiera innerHTML z shadow root
  2. Parsuje go (właściwie,
  3. Normalizuje go (potencjalnie każdy tag/właściwość w osobnej linii)
  4. Parsuje i normalizuje oczekiwany ciąg HTML
  5. Przekazuje oba znormalizowane ciągi DOM do domyślnej funkcji porównywania chai
  6. W przypadku niepowodzenia, grupuje i wyświetla wszelkie różnice w przejrzysty sposób

Jeśli chcesz wiedzieć więcej, sprawdź dokumentację semantic-dom-diff.

Testowanie „lekkiego” DOM

Możemy zrobić dokładnie to samo z lekkim DOM. (DOM, który zostanie dostarczony przez naszego użytkownika lub nasze defaulty, czyli element children).

I zaimplementujmy to.

Tak więc przetestowaliśmy nasz dom światła i cienia 💪 i nasze testy przebiegają czysto 🎉

Uwaga: Używanie DOM API w cyklu życia elementu jest anty-wzorcem, jednak aby umożliwić a11y może to być prawdziwy przypadek użycia – w każdym razie jest to świetne dla celów ilustracyjnych

Używanie naszego elementu w aplikacji

Więc teraz, gdy mamy podstawowe wejście a11y użyjmy go – i przetestujmy – w naszej aplikacji.

Ponownie zaczynamy od szkieletu src/my-app.js

I nasz test w test/my-app.test.js;

Run the test =src/a11y-input.js

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

Ale o nie! To powinno być teraz zielone…

Co się dzieje?
Pamiętasz, że mieliśmy specyficzny test, aby zapewnić light-dom a11y-input?
Więc nawet jeśli użytkownik tylko umieszcza <a11y-input></a11y-input> w swoim kodzie – to co faktycznie wychodzi to

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

np. a11y-input faktycznie tworzy węzły wewnątrz twojej my-app shadow dom. Niedorzeczne! Dla naszego przykładu tutaj mówimy, że tego właśnie chcemy.
Jak więc możemy to jeszcze przetestować?

Na szczęście .shadowDom ma jeszcze jednego asa w rękawie; pozwala nam ignorować części dom.

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

Możemy nawet określić następujące właściwości:

  • ignoreChildren
  • ignoreTags
  • ignoreAttributes (globalnie lub dla konkretnych znaczników)

Więcej szczegółów można znaleźć w semantic-dom-diff.

Testowanie migawek

Jeśli masz wiele dużych drzew dom, pisanie/utrzymywanie tych wszystkich ręcznie napisanych oczekiwań będzie trudne.
Aby ci w tym pomóc, istnieją półautomatyczne migawki.

Więc jeśli zmienimy nasz kod

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

Jeśli teraz wykonamy npm run test to utworzy on plik __snapshots__/a11y input.md i wypełni go czymś takim

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

To co napisaliśmy wcześniej ręcznie może być teraz generowane automatycznie przy inicjacji lub wymuszone przez npm run test:update-snapshots.

Jeśli plik __snapshots__/a11y input.md już istnieje, porówna go z danymi wyjściowymi i otrzymasz błędy, jeśli twoje dane wyjściowe html uległy zmianie.

Po więcej szczegółów proszę zobacz semantic-dom-diff.

Myślę, że to już wystarczająco dużo o porównywaniu drzew dom…
Czas na zmianę 🤗

Pokrycie kodu

Kolejną użyteczną metryką, którą otrzymujemy podczas testowania z konfiguracją open-wc jest pokrycie kodu.
Więc co to znaczy i jak możemy je uzyskać? Pokrycie kodu jest miarą tego, jak duża część naszego kodu jest sprawdzana przez testy. Jeśli istnieje linia, stwierdzenie, funkcja lub gałąź (np. if/else stwierdzenie), której nasze testy nie obejmują, będzie to miało wpływ na nasz wynik pokrycia.
Proste npm run test jest wszystkim, czego potrzebujemy, a otrzymasz następujące wyniki:

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

Co oznacza, że 100% stwierdzeń, gałęzi, funkcji i linii naszego kodu jest objętych testami. Całkiem zgrabnie!

Pójdźmy więc w drugą stronę i dodajmy kod do src/a11y-input.js przed dodaniem testu. Powiedzmy, że chcemy uzyskać dostęp do wartości naszego wejścia bezpośrednio przez nasz niestandardowy element i za każdym razem, gdy jego wartością jest 'cat’, chcemy coś zalogować.

To zupełnie inny wynik

Nasze pokrycie jest dużo niższe niż poprzednio. Nasze polecenie testowe nawet zawodzi, mimo że wszystkie testy przebiegają pomyślnie.
Wynika to z tego, że domyślnie konfiguracja open-wc ustawia 90% próg pokrycia kodu.

Jeśli chcemy poprawić pokrycie, musimy dodać testy – więc zróbmy to

uh oh 😱 chcieliśmy poprawić pokrycie, ale teraz musimy najpierw naprawić rzeczywisty błąd 😞

To było nieoczekiwane… na pierwszy rzut oka, nie bardzo wiem co to oznacza…. lepiej sprawdzić kilka rzeczywistych węzłów i sprawdzić je w przeglądarce.

Debugowanie w przeglądarce

Kiedy uruchamiamy nasz test z watch, karma ustawia trwałe środowisko przeglądarki do uruchamiania testów.

  • Upewnij się, że zacząłeś od npm run test:watch
  • visit http://localhost:9876/debug.html

Powinieneś zobaczyć coś takiego

Możesz kliknąć na okrążony przycisk odtwarzania, aby uruchomić tylko jeden pojedynczy test.

Więc otwórzmy Chrome Dev Tools (F12) i umieśćmy debugger w kodzie testu.

Dang… błąd pojawia się nawet przed tym punktem…
„Fatalne” błędy takie jak ten są trochę trudniejsze, ponieważ nie są to nieudane testy, ale coś w rodzaju kompletnego stopienia się całego komponentu.

Ok, umieśćmy trochę kodu w setter bezpośrednio.

set value(newValue) { debugger;

W porządku, to zadziałało, więc w naszej konsoli chrome piszemy console.log(this) zobaczmy co tu mamy

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

Ahh tam to mamy – dom cieni nie jest jeszcze wyrenderowany kiedy setter jest wywoływany.
Bądźmy więc bezpieczni i dodajmy sprawdzenie przed

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

Błądatel zniknął 🎉
Ale teraz mamy nieudany test 😭

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

Możemy potrzebować zmiany taktyki 🤔
Możemy dodać go jako oddzielną właściwość value i zsynchronizować w razie potrzeby.

I w końcu wracamy do gry! 🎉

ok bug fixed – can we please get back to coverage? thank you 🙏

Back to coverage

Dzięki temu dodanemu testowi zrobiliśmy pewien postęp.

Jednakże wciąż nie jesteśmy w pełni na miejscu – pytanie brzmi dlaczego?

Aby się tego dowiedzieć otwórz coverage/index.html w przeglądarce. Nie potrzebujesz serwera WWW, po prostu otwórz plik w przeglądarce – na mac’u możesz to zrobić z linii poleceń za pomocą open coverage/index.html

Zobaczysz coś takiego

Po kliknięciu na a11y-input.js dostaniesz informację linia po linii jak często są one wykonywane.
Więc możemy od razu zobaczyć, które linie nie są jeszcze wykonywane przez nasze testy.

Dodajmy więc do tego test

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

Dzięki temu jesteśmy z powrotem na 100% w oświadczeniach, ale wciąż brakuje nam czegoś w gałęziach.
Zobaczmy dlaczego?

To E oznacza else path not taken.
Więc za każdym razem gdy funkcja update zostaje wywołana zawsze jest właściwość value w changedProperties.

Mamy też label więc warto to przetestować. 👍

boom 100% 💪 wygrywamy 🥇

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

Ale czekaj nawet nie skończyliśmy powyższego testu – kod nadal jest

 // somehow check that console.log was called

Jak to możliwe, że mamy 100% pokrycia testami?

Najpierw spróbujmy zrozumieć jak działa pokrycie kodu 🤔
Sposób w jaki mierzy się pokrycie kodu polega na zastosowaniu pewnej formy instrumentation. W skrócie, zanim nasz kod zostanie wykonany, zostaje zmieniony (instrumented) i zachowuje się mniej więcej tak:

Uwaga: To jest super uproszczona wersja dla celów ilustracyjnych.

Podstawowo, twój kod zostaje zaśmiecony wieloma flagami. Na podstawie tego, które flagi zostaną wywołane, tworzona jest statystyka.

Więc 100% pokrycie testami oznacza tylko, że każda linia twojego kodu została wykonana przynajmniej raz po zakończeniu wszystkich testów. Nie oznacza to, że przetestowałeś wszystko, lub że twoje testy wykonują poprawne asercje.

Więc nawet jeśli mamy już 100% pokrycia kodu, nadal zamierzamy poprawić nasz test dziennika.

Powinieneś więc postrzegać pokrycie kodu jako narzędzie, które tylko daje ci wskazówki i pomoc w wykryciu niektórych brakujących testów, a nie jako twardą gwarancję jakości kodu.

Szpiegowanie kodu

Jeśli chcesz sprawdzić jak często lub z jakimi parametrami funkcja jest wywoływana, to nazywamy to szpiegowaniem.
open-wc zaleca czcigodny pakiet sinon, który dostarcza wielu narzędzi do szpiegowania i innych powiązanych zadań.

npm i -D sinon

Tworzysz więc szpiega na określonym obiekcie, a następnie możesz sprawdzić, jak często jest on wywoływany.

Uh oh…. test się nie powiódł:

AssertionError: expected 0 to equal 1

Mieszanie z globalnymi obiektami jak console może mieć skutki uboczne, więc lepiej refaktoryzujmy używając dedykowanej funkcji log.

To skutkuje brakiem globalnego obiektu w naszym kodzie testowym – słodkie 🤗

Jednakże wciąż dostajemy ten sam błąd. Let’s debug… boohoo apparently update is not sync – a wrong assumption I made 🙈 I am saying assumptions are dangerous quite often – still I fall for it from time to time 😢.

So what can we do? Niestety wydaje się, że nie ma publicznego api, aby wykonać niektóre akcje synchronizacji wywołane przez aktualizację właściwości.
Utwórzmy kwestię dla tego https://github.com/Polymer/lit-element/issues/643.

Na razie najwyraźniej jedynym sposobem jest poleganie na prywatnym api. 🙈
Potrzebowaliśmy też przenieść value sync do updated, żeby było wykonywane po każdym renderowaniu dom.

i tutaj jest zaktualizowany test dla logowania

wow, to było trochę trudniejsze niż się spodziewaliśmy, ale udało się 💪

SUMMARY:✔ 7 tests completedTOTAL: 7 SUCCESS

Uruchamianie testów bez frameworka Karma

Framework Karma jest potężny i bogaty w funkcje, ale czasami możemy chcieć zmniejszyć nasz regiment testowy. Fajną rzeczą we wszystkim co zaproponowaliśmy do tej pory jest to, że użyliśmy tylko standardowych dla przeglądarki modułów es bez potrzeby transpilacji, z jednym wyjątkiem specyfikatorów gołych modułów.
Tak więc po prostu tworząc test/index.html.

i otwierając go przez owc-dev-server w chrome, będzie on działał doskonale.
Wszystko działa bez webpack czy karma – słodko 🤗

Do the Cross-Browser Thing

Czujemy się teraz całkiem komfortowo z naszym komponentem webowym. Jest przetestowany i pokryty; jest jeszcze tylko jeden krok – chcemy się upewnić, że działa i jest przetestowany we wszystkich przeglądarkach.

Więc po prostu go uruchommy

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

Tak, to działa ładnie! 🤗

Jeśli są nieudane testy, wyświetli je w podsumowaniu wraz z konkretną przeglądarką, w której się nie powiodły.

Jeśli potrzebujesz debugować konkretną przeglądarkę:

Jeśli chcesz dostosować przeglądarkę, która jest testowana, możesz dostosować swoje karma.bs.config.js.

Na przykład, jeśli chcesz dodać Firefox ESR do swojej listy.

A może chcesz przetestować tylko 2 konkretne przeglądarki?

Uwaga: To wykorzystuje strategie webpack merge replace.

Szybkie podsumowanie

  • Testowanie jest ważne dla każdego projektu. Postaraj się napisać ich jak najwięcej.
  • Postaraj się utrzymać wysoki poziom pokrycia kodu, ale pamiętaj, że nie jest to magiczna gwarancja, więc nie zawsze musi wynosić 100%.
  • Debuguj w przeglądarce przez npm run test:watch. Dla starszych przeglądarek, użyj npm run test:legacy.watch.

Co dalej?

  • Uruchom testy w swoim CI (działa doskonale razem z browserstack). Zobacz nasze zalecenia dotyczące automatyzacji.

Śledź nas na Twitterze, lub śledź mnie na moim osobistym Twitterze.
Zapewnij się, że sprawdzisz nasze inne narzędzia i rekomendacje na open-wc.org.

Dzięki Pascalowi i Benny’emu za informacje zwrotne i pomoc w przekształceniu moich bazgrołów w nadającą się do naśladowania historię.