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()?
- Pobiera
innerHTML
z shadow root - Parsuje go (właściwie,
- Normalizuje go (potencjalnie każdy tag/właściwość w osobnej linii)
- Parsuje i normalizuje oczekiwany ciąg HTML
- Przekazuje oba znormalizowane ciągi DOM do domyślnej funkcji porównywania chai
- 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żyjnpm 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ę.