Picasso: jak przetestować bibliotekę komponentów

Opublikowany: 2022-03-11

Niedawno została wydana nowa wersja systemu projektowania firmy Toptal, która wymagała wprowadzenia zmian w prawie każdym komponencie Picassa, naszej wewnętrznej biblioteki komponentów. Nasz zespół stanął przed wyzwaniem: Jak zapewnić, że regresje się nie pojawią?

Krótka odpowiedź to raczej testy. Wiele testów.

Nie będziemy weryfikować teoretycznych aspektów testowania, omawiać różnych typów testów, ich użyteczności ani wyjaśniać, dlaczego w pierwszej kolejności powinieneś testować swój kod. Nasz blog i inni już omówili te tematy. Zamiast tego skupimy się wyłącznie na praktycznych aspektach testowania.

Czytaj dalej, aby dowiedzieć się, jak testują programiści w Toptal. Nasze repozytorium jest publiczne, więc korzystamy z przykładów ze świata rzeczywistego. Nie ma żadnych abstrakcji ani uproszczeń.

Piramida testowa

Nie mamy zdefiniowanej piramidy testowej per se, ale gdybyśmy to zrobili, wyglądałoby to tak:

Testowanie ilustracji piramidy

Piramida testowa Toptal ilustruje testy, które kładziemy nacisk.

Testy jednostkowe

Testy jednostkowe są proste do napisania i łatwe do uruchomienia. Jeśli masz bardzo mało czasu na pisanie testów, powinny one być Twoim pierwszym wyborem.

Nie są jednak idealne. Niezależnie od wybranej biblioteki testowej (w naszym przypadku Jest i React Testing Library [RTL]), nie będzie ona miała prawdziwego DOM-a i nie pozwoli sprawdzić funkcjonalności w różnych przeglądarkach, ale pozwoli rozebrać odrzuć złożoność i przetestuj proste elementy składowe swojej biblioteki.

Testy jednostkowe nie tylko dodają wartości, testując zachowanie kodu, ale także sprawdzając ogólną testowalność kodu. Jeśli nie możesz łatwo napisać testów jednostkowych, prawdopodobnie masz zły kod.

Testy regresji wizualnej

Nawet jeśli masz 100% pokrycia testów jednostkowych, nie oznacza to, że komponenty wyglądają dobrze na różnych urządzeniach i przeglądarkach.

Regresje wizualne są szczególnie trudne do wykrycia w przypadku testów manualnych. Na przykład, jeśli etykieta przycisku zostanie przesunięta o 1 piksel, czy inżynier QA w ogóle to zauważy? Na szczęście istnieje wiele rozwiązań tego problemu ograniczonej widoczności. Możesz zdecydować się na kompleksowe rozwiązania klasy korporacyjnej, takie jak LambdaTest lub Mabl. Możesz włączyć wtyczki, takie jak Percy, do swoich istniejących testów, a także rozwiązania DIY od Lokiego lub Storybook (którego używaliśmy przed Picasso). Wszystkie mają wady: niektóre są zbyt drogie, podczas gdy inne mają stromą krzywą uczenia się lub wymagają zbyt wiele konserwacji.

Happo na ratunek! Jest bezpośrednim konkurentem Percy'ego, ale jest znacznie tańszy, obsługuje więcej przeglądarek i jest łatwiejszy w użyciu. Kolejny duży punkt sprzedaży? Obsługuje integrację Cypress, co było ważne, ponieważ chcieliśmy odejść od używania Storybook do testowania wizualnego. Znaleźliśmy się w sytuacjach, w których musieliśmy tworzyć historie tylko po to, aby zapewnić pokrycie testu wizualnego, a nie dlatego, że musieliśmy udokumentować ten przypadek użycia. Zanieczyściło to nasze dokumenty i utrudniło ich zrozumienie. Chcieliśmy oddzielić testy wizualne od dokumentacji wizualnej.

Testy integracyjne

Nawet jeśli dwa komponenty mają testy jednostkowe i wizualne, nie gwarantuje to, że będą ze sobą współpracować. Na przykład znaleźliśmy błąd, w którym podpowiedź nie otwiera się, gdy jest używana w elemencie rozwijanym, ale działa dobrze, gdy jest używana samodzielnie.

Aby zapewnić dobrą integrację komponentów, wykorzystaliśmy eksperymentalną funkcję testowania komponentów Cypress. Na początku byliśmy niezadowoleni ze słabej wydajności, ale udało nam się ją ulepszyć za pomocą niestandardowej konfiguracji webpacka. Wynik? Udało nam się wykorzystać doskonałe API Cypress do napisania wydajnych testów, które zapewnią, że nasze komponenty będą dobrze ze sobą współpracować.

Zastosowanie Piramidy Testów

Jak to wszystko wygląda w prawdziwym życiu? Przetestujmy komponent Akordeon!

Twoim pierwszym odruchem może być otwarcie edytora i rozpoczęcie pisania kodu. Moja rada? Poświęć trochę czasu na zrozumienie wszystkich funkcji komponentu i zapisz przypadki testowe, które chcesz uwzględnić.

Prezentacja biblioteki komponentów Picassa GIF

Co przetestować?

Oto zestawienie przypadków, które powinny obejmować nasze testy:

  • Stany – akordeony można rozwijać i zwijać, można skonfigurować ich stan domyślny i wyłączyć tę funkcję
  • Style – akordeony mogą mieć różne warianty obramowania
  • Treść – Mogą integrować się z innymi jednostkami biblioteki
  • Dostosowywanie – komponent może mieć nadpisane style i może mieć niestandardowe ikony rozwijania
  • Callbacki – Za każdym razem, gdy zmienia się stan, można wywołać callback

Prezentacja biblioteki komponentów Picassa GIF - komponent akordeonu

Jak testować?

Teraz, gdy wiemy, co musimy przetestować, zastanówmy się, jak się do tego zabrać. Z naszej piramidy testowej mamy trzy opcje. Chcemy osiągnąć maksymalne pokrycie przy minimalnym nakładaniu się części piramidy. Jaki jest najlepszy sposób przetestowania każdego przypadku testowego?

  • Stany — testy jednostkowe mogą nam pomóc ocenić, czy stany odpowiednio się zmieniają, ale potrzebujemy również testów wizualnych, aby upewnić się, że komponent jest renderowany poprawnie w każdym stanie
  • Style – testy wizualne powinny wystarczyć do wykrycia regresji różnych wariantów
  • Treść – Połączenie testów wizualnych i integracyjnych to najlepszy wybór, ponieważ akordeony mogą być używane w połączeniu z wieloma innymi komponentami
  • Personalizacja – możemy użyć testu jednostkowego, aby sprawdzić, czy nazwa klasy jest zastosowana poprawnie, ale potrzebujemy testu wizualnego, aby upewnić się, że styl komponentu i niestandardowy działają w tandemie
  • Callbacki – Testy jednostkowe są idealne do zapewnienia wywoływania właściwych callbacków

Piramida testowania akordeonu

Testy jednostkowe

Pełny zestaw testów jednostkowych można znaleźć tutaj. Omówiliśmy wszystkie zmiany stanu, dostosowania i wywołania zwrotne:

 it('toggles', async () => { const handleChange = jest.fn() const { getByText, getByTestId } = renderAccordion({ onChange: handleChange, expandIcon: <span data-test /> }) fireEvent.click(getByTestId('accordion-summary')) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) fireEvent.click(getByTestId('trigger')) await waitFor(() => expect(getByText(DETAILS_TEXT)).not.toBeVisible()) fireEvent.click(getByText(SUMMARY_TEXT)) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) expect(handleChange).toHaveBeenCalledTimes(3) })

Testy regresji wizualnej

Testy wizualne znajdują się w tym bloku opisu Cypress. Zrzuty ekranu można znaleźć na pulpicie nawigacyjnym Happo.

Możesz zobaczyć, jak zostały zarejestrowane wszystkie różne stany, warianty i dostosowania komponentów. Za każdym razem, gdy PR jest otwierany, CI porównuje zrzuty ekranu, które zapisał Happo, z tymi zrobionymi w Twoim oddziale:

 it('renders', () => { mount( <TestingPicasso> <TestAccordion /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders disabled', () => { mount( <TestingPicasso> <TestAccordion disabled /> <TestAccordion expandIcon={<Check16 />} /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders border variants', () => { mount( <TestingPicasso> <TestAccordion borders='none' /> <TestAccordion borders='middle' /> <TestAccordion borders='all' /> </TestingPicasso> ) cy.get('body').happoScreenshot() })

Testy integracyjne

Napisaliśmy test „złej ścieżki” w tym bloku opisu Cypress, który potwierdza, że ​​Akordeon nadal działa poprawnie i że użytkownicy mogą wchodzić w interakcje z komponentem niestandardowym. Dodaliśmy również wizualne asercje dla dodatkowej pewności:

 describe('Accordion with custom summary', () => { it('closes and opens', () => { mount(<AccordionCustomSummary />) toggleAccordion() getAccordionContent().should('not.be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() toggleAccordion() getAccordionContent().should('be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() }) // … })

Ciągła integracja

Picasso opiera się prawie całkowicie na GitHub Actions w zakresie kontroli jakości. Dodatkowo dodaliśmy zaczepy Git do sprawdzania jakości kodu w plikach pomostowych. Niedawno przenieśliśmy się z Jenkins do GHA, więc nasza konfiguracja jest nadal na etapie MVP.

Przepływ pracy jest uruchamiany przy każdej zmianie w zdalnym oddziale w kolejności sekwencyjnej, przy czym ostatnim etapem są testy integracyjne i wizualne, ponieważ są one najdroższe w uruchomieniu (zarówno pod względem wydajności, jak i kosztów). Jeśli wszystkie testy nie zakończą się pomyślnie, żądanie ściągnięcia nie może zostać scalone.

Oto etapy, przez które przechodzi GitHub Actions za każdym razem:

  1. Instalacja zależności
  2. Kontrola wersji — sprawdza, czy format zatwierdzeń i tytuł PR odpowiadają konwencjonalnym zatwierdzeniom
  3. Lint – ESlint zapewnia kod dobrej jakości
  4. Kompilacja TypeScript – Sprawdź, czy nie ma błędów typu
  5. Kompilacja pakietów — jeśli nie można zbudować pakietów, nie zostaną one pomyślnie wydane; nasze testy Cypressa oczekują również skompilowanego kodu
  6. Testy jednostkowe
  7. Testy integracyjne i wizualne

Pełny przepływ pracy można znaleźć tutaj. Obecnie przejście wszystkich etapów zajmuje mniej niż 12 minut.

Testowalność

Podobnie jak większość bibliotek komponentów, Picasso ma komponent główny, który musi otaczać wszystkie inne komponenty i może być używany do ustawiania reguł globalnych. Utrudnia to pisanie testów z dwóch powodów — niespójności w wynikach testów, w zależności od właściwości użytych w wrapperze; i dodatkowa płyta kotła:

 import { render } from '@testing-library/react' describe('Form', () => { it('renders', () => { const { container } = render( <Picasso loadFavicon={false} environment='test'> <Form /> </Picasso> ) expect(container).toMatchSnapshot() }) })

Rozwiązaliśmy pierwszy problem, tworząc TestingPicasso, który warunkuje globalne reguły testowania. Ale denerwujące jest deklarowanie tego dla każdego przypadku testowego. Dlatego stworzyliśmy niestandardową funkcję renderowania, która opakowuje przekazany komponent w TestingPicasso i zwraca wszystko, co jest dostępne z funkcji renderowania RTL.

Nasze testy są teraz łatwiejsze do odczytania i prostsze do napisania:

 import { render } from '@toptal/picasso/test-utils' describe('Form', () => { it('renders', () => { const { container } = render(<Form />) expect(container).toMatchSnapshot() }) })

Wniosek

Opisana tutaj konfiguracja jest daleka od ideału, ale jest dobrym punktem wyjścia dla tych, którzy są na tyle odważni, aby stworzyć bibliotekę komponentów. Dużo czytałem o testowaniu piramid, ale zastosowanie ich w praktyce nie zawsze jest łatwe. Dlatego zapraszam do zapoznania się z naszą bazą kodów i uczenia się na naszych błędach i sukcesach.

Biblioteki komponentów są unikalne, ponieważ służą dwóm rodzajom odbiorców: użytkownikom końcowym, którzy wchodzą w interakcję z interfejsem użytkownika oraz programistom, którzy używają kodu do tworzenia własnych aplikacji. Inwestowanie czasu w solidne ramy testowe przyniesie korzyści wszystkim. Inwestowanie czasu w ulepszenia testowalności przyniesie korzyści jako opiekunowi i inżynierom, którzy używają (i testują) twoją bibliotekę.

Nie omawialiśmy takich rzeczy, jak pokrycie kodu, kompleksowe testy oraz zasady dotyczące wersji i wydania. Krótka rada na te tematy to: publikuj często, ćwicz właściwe wersjonowanie semantyczne, zachowaj przejrzystość procesów i ustaw oczekiwania dla inżynierów, którzy polegają na twojej bibliotece. W kolejnych postach możemy wrócić do tych tematów bardziej szczegółowo.